diff --git a/.travis.yml b/.travis.yml
index c8f35b41b2..ab6c2d09b8 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -185,7 +185,7 @@ install:
- |
# Install the phpunit-bridge from a PR if required
#
- # To run a PR with a patched phpunit-bridge, first submit the path for the
+ # To run a PR with a patched phpunit-bridge, first submit the patch for the
# phpunit-bridge as a separate PR against the next feature-branch then
# uncomment and update the following line with that PR number
#SYMFONY_PHPUNIT_BRIDGE_PR=32886
diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/PolyfillAssertTrait.php b/src/Symfony/Bridge/PhpUnit/Legacy/PolyfillAssertTrait.php
index 5d564774a7..69a01d2927 100644
--- a/src/Symfony/Bridge/PhpUnit/Legacy/PolyfillAssertTrait.php
+++ b/src/Symfony/Bridge/PhpUnit/Legacy/PolyfillAssertTrait.php
@@ -17,7 +17,7 @@ use PHPUnit\Framework\Constraint\StringContains;
use PHPUnit\Framework\Constraint\TraversableContains;
/**
- * This trait is @internal
+ * This trait is @internal.
*/
trait PolyfillAssertTrait
{
@@ -251,4 +251,196 @@ trait PolyfillAssertTrait
static::assertInternalType('float', $actual, $message);
static::assertTrue(is_nan($actual), $message ? $message : "Failed asserting that $actual is nan.");
}
+
+ /**
+ * @param string $filename
+ * @param string $message
+ *
+ * @return void
+ */
+ public static function assertIsReadable($filename, $message = '')
+ {
+ static::assertInternalType('string', $filename, $message);
+ static::assertTrue(is_readable($filename), $message ? $message : "Failed asserting that $filename is readable.");
+ }
+
+ /**
+ * @param string $filename
+ * @param string $message
+ *
+ * @return void
+ */
+ public static function assertNotIsReadable($filename, $message = '')
+ {
+ static::assertInternalType('string', $filename, $message);
+ static::assertFalse(is_readable($filename), $message ? $message : "Failed asserting that $filename is not readable.");
+ }
+
+ /**
+ * @param string $filename
+ * @param string $message
+ *
+ * @return void
+ */
+ public static function assertIsWritable($filename, $message = '')
+ {
+ static::assertInternalType('string', $filename, $message);
+ static::assertTrue(is_writable($filename), $message ? $message : "Failed asserting that $filename is writable.");
+ }
+
+ /**
+ * @param string $filename
+ * @param string $message
+ *
+ * @return void
+ */
+ public static function assertNotIsWritable($filename, $message = '')
+ {
+ static::assertInternalType('string', $filename, $message);
+ static::assertFalse(is_writable($filename), $message ? $message : "Failed asserting that $filename is not writable.");
+ }
+
+ /**
+ * @param string $directory
+ * @param string $message
+ *
+ * @return void
+ */
+ public static function assertDirectoryExists($directory, $message = '')
+ {
+ static::assertInternalType('string', $directory, $message);
+ static::assertTrue(is_dir($directory), $message ? $message : "Failed asserting that $directory exists.");
+ }
+
+ /**
+ * @param string $directory
+ * @param string $message
+ *
+ * @return void
+ */
+ public static function assertDirectoryNotExists($directory, $message = '')
+ {
+ static::assertInternalType('string', $directory, $message);
+ static::assertFalse(is_dir($directory), $message ? $message : "Failed asserting that $directory does not exist.");
+ }
+
+ /**
+ * @param string $directory
+ * @param string $message
+ *
+ * @return void
+ */
+ public static function assertDirectoryIsReadable($directory, $message = '')
+ {
+ static::assertDirectoryExists($directory, $message);
+ static::assertIsReadable($directory, $message);
+ }
+
+ /**
+ * @param string $directory
+ * @param string $message
+ *
+ * @return void
+ */
+ public static function assertDirectoryNotIsReadable($directory, $message = '')
+ {
+ static::assertDirectoryExists($directory, $message);
+ static::assertNotIsReadable($directory, $message);
+ }
+
+ /**
+ * @param string $directory
+ * @param string $message
+ *
+ * @return void
+ */
+ public static function assertDirectoryIsWritable($directory, $message = '')
+ {
+ static::assertDirectoryExists($directory, $message);
+ static::assertIsWritable($directory, $message);
+ }
+
+ /**
+ * @param string $directory
+ * @param string $message
+ *
+ * @return void
+ */
+ public static function assertDirectoryNotIsWritable($directory, $message = '')
+ {
+ static::assertDirectoryExists($directory, $message);
+ static::assertNotIsWritable($directory, $message);
+ }
+
+ /**
+ * @param string $filename
+ * @param string $message
+ *
+ * @return void
+ */
+ public static function assertFileExists($filename, $message = '')
+ {
+ static::assertInternalType('string', $filename, $message);
+ static::assertTrue(file_exists($filename), $message ? $message : "Failed asserting that $filename exists.");
+ }
+
+ /**
+ * @param string $filename
+ * @param string $message
+ *
+ * @return void
+ */
+ public static function assertFileNotExists($filename, $message = '')
+ {
+ static::assertInternalType('string', $filename, $message);
+ static::assertFalse(file_exists($filename), $message ? $message : "Failed asserting that $filename does not exist.");
+ }
+
+ /**
+ * @param string $filename
+ * @param string $message
+ *
+ * @return void
+ */
+ public static function assertFileIsReadable($filename, $message = '')
+ {
+ static::assertFileExists($filename, $message);
+ static::assertIsReadable($filename, $message);
+ }
+
+ /**
+ * @param string $filename
+ * @param string $message
+ *
+ * @return void
+ */
+ public static function assertFileNotIsReadable($filename, $message = '')
+ {
+ static::assertFileExists($filename, $message);
+ static::assertNotIsReadable($filename, $message);
+ }
+
+ /**
+ * @param string $filename
+ * @param string $message
+ *
+ * @return void
+ */
+ public static function assertFileIsWritable($filename, $message = '')
+ {
+ static::assertFileExists($filename, $message);
+ static::assertIsWritable($filename, $message);
+ }
+
+ /**
+ * @param string $filename
+ * @param string $message
+ *
+ * @return void
+ */
+ public static function assertFileNotIsWritable($filename, $message = '')
+ {
+ static::assertFileExists($filename, $message);
+ static::assertNotIsWritable($filename, $message);
+ }
}
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
index 19721fb871..196d7d6c7e 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
@@ -528,6 +528,10 @@ class FrameworkExtension extends Extension
$loader->load('messenger_debug.xml');
}
+ if (class_exists(Mailer::class)) {
+ $loader->load('mailer_debug.xml');
+ }
+
$container->setParameter('profiler_listener.only_exceptions', $config['only_exceptions']);
$container->setParameter('profiler_listener.only_master_requests', $config['only_master_requests']);
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_debug.xml
new file mode 100644
index 0000000000..17ed496661
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_debug.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Symfony/Component/Mailer/CHANGELOG.md b/src/Symfony/Component/Mailer/CHANGELOG.md
index 6a0669faca..76b895a680 100644
--- a/src/Symfony/Component/Mailer/CHANGELOG.md
+++ b/src/Symfony/Component/Mailer/CHANGELOG.md
@@ -4,6 +4,7 @@ CHANGELOG
4.4.0
-----
+ * Added `MessageEvents` and `MessageLoggerListener` to allow collecting sent emails
* [BC BREAK] `TransportInterface` has a new `getName()` method
* [BC BREAK] Classes `AbstractApiTransport` and `AbstractHttpTransport` moved under `Transport` sub-namespace.
* [BC BREAK] Transports depend on `Symfony\Contracts\EventDispatcher\EventDispatcherInterface`
diff --git a/src/Symfony/Component/Mailer/Event/MessageEvent.php b/src/Symfony/Component/Mailer/Event/MessageEvent.php
index 41c31d442a..84be56d999 100644
--- a/src/Symfony/Component/Mailer/Event/MessageEvent.php
+++ b/src/Symfony/Component/Mailer/Event/MessageEvent.php
@@ -16,7 +16,7 @@ use Symfony\Component\Mime\RawMessage;
use Symfony\Contracts\EventDispatcher\Event;
/**
- * Allows the transformation of a Message.
+ * Allows the transformation of a Message and the SMTP Envelope before the email is sent.
*
* @author Fabien Potencier
*/
@@ -24,11 +24,15 @@ class MessageEvent extends Event
{
private $message;
private $envelope;
+ private $transportName;
+ private $queued;
- public function __construct(RawMessage $message, SmtpEnvelope $envelope)
+ public function __construct(RawMessage $message, SmtpEnvelope $envelope, string $transportName, bool $queued = false)
{
$this->message = $message;
$this->envelope = $envelope;
+ $this->transportName = $transportName;
+ $this->queued = $queued;
}
public function getMessage(): RawMessage
@@ -50,4 +54,14 @@ class MessageEvent extends Event
{
$this->envelope = $envelope;
}
+
+ public function getTransportName(): string
+ {
+ return $this->transportName;
+ }
+
+ public function isQueued(): bool
+ {
+ return $this->queued;
+ }
}
diff --git a/src/Symfony/Component/Mailer/Event/MessageEvents.php b/src/Symfony/Component/Mailer/Event/MessageEvents.php
new file mode 100644
index 0000000000..a921022ced
--- /dev/null
+++ b/src/Symfony/Component/Mailer/Event/MessageEvents.php
@@ -0,0 +1,51 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Mailer\Event;
+
+/**
+ * @author Fabien Potencier
+ */
+class MessageEvents
+{
+ private $events = [];
+ private $transports = [];
+
+ public function add(MessageEvent $event): void
+ {
+ $this->events[] = $event;
+ $this->transports[$event->getTransportName()] = true;
+ }
+
+ public function getTransports(): array
+ {
+ return array_keys($this->transports);
+ }
+
+ /**
+ * @return MessageEvent[]
+ */
+ public function getEvents(string $name = null): array
+ {
+ if (null === $name) {
+ return $this->events;
+ }
+
+ $events = [];
+ foreach ($this->events as $event) {
+ if ($name === $event->getTransportName()) {
+ $events[] = $event;
+ }
+ }
+
+ return $events;
+ }
+}
diff --git a/src/Symfony/Component/Mailer/EventListener/MessageLoggerListener.php b/src/Symfony/Component/Mailer/EventListener/MessageLoggerListener.php
new file mode 100644
index 0000000000..093bf2bb9e
--- /dev/null
+++ b/src/Symfony/Component/Mailer/EventListener/MessageLoggerListener.php
@@ -0,0 +1,57 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Mailer\EventListener;
+
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\Mailer\Event\MessageEvent;
+use Symfony\Component\Mailer\Event\MessageEvents;
+use Symfony\Contracts\Service\ResetInterface;
+
+/**
+ * Logs Messages.
+ *
+ * @author Fabien Potencier
+ */
+class MessageLoggerListener implements EventSubscriberInterface, ResetInterface
+{
+ private $events;
+
+ public function __construct()
+ {
+ $this->events = new MessageEvents();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function reset()
+ {
+ $this->events = new MessageEvents();
+ }
+
+ public function onMessage(MessageEvent $event): void
+ {
+ $this->events->add($event);
+ }
+
+ public function getEvents(): MessageEvents
+ {
+ return $this->events;
+ }
+
+ public static function getSubscribedEvents()
+ {
+ return [
+ MessageEvent::class => ['onMessage', -255],
+ ];
+ }
+}
diff --git a/src/Symfony/Component/Mailer/Mailer.php b/src/Symfony/Component/Mailer/Mailer.php
index 324f50cad7..db87602022 100644
--- a/src/Symfony/Component/Mailer/Mailer.php
+++ b/src/Symfony/Component/Mailer/Mailer.php
@@ -11,6 +11,8 @@
namespace Symfony\Component\Mailer;
+use Symfony\Component\Mailer\Event\MessageEvent;
+use Symfony\Component\Mailer\Exception\TransportException;
use Symfony\Component\Mailer\Messenger\SendEmailMessage;
use Symfony\Component\Mailer\Transport\TransportInterface;
use Symfony\Component\Messenger\MessageBusInterface;
@@ -38,6 +40,19 @@ class Mailer implements MailerInterface
return;
}
+ $message = clone $message;
+ if (null !== $envelope) {
+ $envelope = clone $envelope;
+ } else {
+ try {
+ $envelope = new DelayedSmtpEnvelope($message);
+ } catch (\Exception $e) {
+ throw new TransportException('Cannot send message without a valid envelope.', 0, $e);
+ }
+ }
+ $event = new MessageEvent($message, $envelope, $this->transport->getName());
+ $this->dispatcher->dispatch($event);
+
$this->bus->dispatch(new SendEmailMessage($message, $envelope));
}
}
diff --git a/src/Symfony/Component/Mailer/Transport/AbstractTransport.php b/src/Symfony/Component/Mailer/Transport/AbstractTransport.php
index 3dd59c195c..90b04c19ab 100644
--- a/src/Symfony/Component/Mailer/Transport/AbstractTransport.php
+++ b/src/Symfony/Component/Mailer/Transport/AbstractTransport.php
@@ -69,7 +69,7 @@ abstract class AbstractTransport implements TransportInterface
}
}
- $event = new MessageEvent($message, $envelope);
+ $event = new MessageEvent($message, $envelope, $this->getName(), true);
$this->dispatcher->dispatch($event);
$envelope = $event->getEnvelope();
if (!$envelope->getRecipients()) {
diff --git a/src/Symfony/Component/Mailer/composer.json b/src/Symfony/Component/Mailer/composer.json
index f9790c2f8a..896f6380c1 100644
--- a/src/Symfony/Component/Mailer/composer.json
+++ b/src/Symfony/Component/Mailer/composer.json
@@ -20,7 +20,8 @@
"egulias/email-validator": "^2.0",
"psr/log": "~1.0",
"symfony/event-dispatcher": "^4.4|^5.0",
- "symfony/mime": "^4.4|^5.0"
+ "symfony/mime": "^4.4|^5.0",
+ "symfony/service-contracts": "^1.1"
},
"require-dev": {
"symfony/amazon-mailer": "^4.4|^5.0",