This commit is contained in:
cutemeli
2025-12-22 10:35:30 +00:00
parent 0bfc6c8425
commit 5ce7ca2c5d
38927 changed files with 0 additions and 4594700 deletions

View File

@@ -1,5 +0,0 @@
CHANGELOG
=========
The changelog is maintained for all Symfony contracts at the following URL:
https://github.com/symfony/contracts/blob/main/CHANGELOG.md

View File

@@ -1,19 +0,0 @@
Copyright (c) 2020-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -1,26 +0,0 @@
Symfony Deprecation Contracts
=============================
A generic function and convention to trigger deprecation notices.
This package provides a single global function named `trigger_deprecation()` that triggers silenced deprecation notices.
By using a custom PHP error handler such as the one provided by the Symfony ErrorHandler component,
the triggered deprecations can be caught and logged for later discovery, both on dev and prod environments.
The function requires at least 3 arguments:
- the name of the Composer package that is triggering the deprecation
- the version of the package that introduced the deprecation
- the message of the deprecation
- more arguments can be provided: they will be inserted in the message using `printf()` formatting
Example:
```php
trigger_deprecation('symfony/blockchain', '8.9', 'Using "%s" is deprecated, use "%s" instead.', 'bitcoin', 'fabcoin');
```
This will generate the following message:
`Since symfony/blockchain 8.9: Using "bitcoin" is deprecated, use "fabcoin" instead.`
While not recommended, the deprecation notices can be completely ignored by declaring an empty
`function trigger_deprecation() {}` in your application.

View File

@@ -1,35 +0,0 @@
{
"name": "symfony/deprecation-contracts",
"type": "library",
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=8.1"
},
"autoload": {
"files": [
"function.php"
]
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-main": "3.6-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
}
}

View File

@@ -1,27 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (!function_exists('trigger_deprecation')) {
/**
* Triggers a silenced deprecation notice.
*
* @param string $package The name of the Composer package that is triggering the deprecation
* @param string $version The version of the package that introduced the deprecation
* @param string $message The message of the deprecation
* @param mixed ...$args Values to insert in the message using printf() formatting
*
* @author Nicolas Grekas <p@tchwork.com>
*/
function trigger_deprecation(string $package, string $version, string $message, mixed ...$args): void
{
@trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED);
}
}

View File

@@ -1,5 +0,0 @@
CHANGELOG
=========
The changelog is maintained for all Symfony contracts at the following URL:
https://github.com/symfony/contracts/blob/main/CHANGELOG.md

View File

@@ -1,51 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\EventDispatcher;
use Psr\EventDispatcher\StoppableEventInterface;
/**
* Event is the base class for classes containing event data.
*
* This class contains no event data. It is used by events that do not pass
* state information to an event handler when an event is raised.
*
* You can call the method stopPropagation() to abort the execution of
* further listeners in your event listener.
*
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
class Event implements StoppableEventInterface
{
private bool $propagationStopped = false;
public function isPropagationStopped(): bool
{
return $this->propagationStopped;
}
/**
* Stops the propagation of the event to further event listeners.
*
* If multiple event listeners are connected to the same event, no
* further event listener will be triggered once any trigger calls
* stopPropagation().
*/
public function stopPropagation(): void
{
$this->propagationStopped = true;
}
}

View File

@@ -1,33 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\EventDispatcher;
use Psr\EventDispatcher\EventDispatcherInterface as PsrEventDispatcherInterface;
/**
* Allows providing hooks on domain-specific lifecycles by dispatching events.
*/
interface EventDispatcherInterface extends PsrEventDispatcherInterface
{
/**
* Dispatches an event to all registered listeners.
*
* @template T of object
*
* @param T $event The event to pass to the event handlers/listeners
* @param string|null $eventName The name of the event to dispatch. If not supplied,
* the class of $event should be used instead.
*
* @return T The passed $event MUST be returned
*/
public function dispatch(object $event, ?string $eventName = null): object;
}

View File

@@ -1,19 +0,0 @@
Copyright (c) 2018-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -1,9 +0,0 @@
Symfony EventDispatcher Contracts
=================================
A set of abstractions extracted out of the Symfony components.
Can be used to build on semantics that the Symfony components proved useful and
that already have battle tested implementations.
See https://github.com/symfony/contracts/blob/main/README.md for more information.

View File

@@ -1,35 +0,0 @@
{
"name": "symfony/event-dispatcher-contracts",
"type": "library",
"description": "Generic abstractions related to dispatching event",
"keywords": ["abstractions", "contracts", "decoupling", "interfaces", "interoperability", "standards"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=8.1",
"psr/event-dispatcher": "^1"
},
"autoload": {
"psr-4": { "Symfony\\Contracts\\EventDispatcher\\": "" }
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-main": "3.6-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
}
}

View File

@@ -1,35 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\EventDispatcher\Attribute;
/**
* Service tag to autoconfigure event listeners.
*
* @author Alexander M. Turek <me@derrabus.de>
*/
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class AsEventListener
{
/**
* @param string|null $event The event name to listen to
* @param string|null $method The method to run when the listened event is triggered
* @param int $priority The priority of this listener if several are declared for the same event
* @param string|null $dispatcher The service id of the event dispatcher to listen to
*/
public function __construct(
public ?string $event = null,
public ?string $method = null,
public int $priority = 0,
public ?string $dispatcher = null,
) {
}
}

View File

@@ -1,96 +0,0 @@
CHANGELOG
=========
6.0
---
* Remove `LegacyEventDispatcherProxy`
5.4
---
* Allow `#[AsEventListener]` attribute on methods
5.3
---
* Add `#[AsEventListener]` attribute for declaring listeners on PHP 8
5.1.0
-----
* The `LegacyEventDispatcherProxy` class has been deprecated.
* Added an optional `dispatcher` attribute to the listener and subscriber tags in `RegisterListenerPass`.
5.0.0
-----
* The signature of the `EventDispatcherInterface::dispatch()` method has been changed to `dispatch($event, string $eventName = null): object`.
* The `Event` class has been removed in favor of `Symfony\Contracts\EventDispatcher\Event`.
* The `TraceableEventDispatcherInterface` has been removed.
* The `WrappedListener` class is now final.
4.4.0
-----
* `AddEventAliasesPass` has been added, allowing applications and bundles to extend the event alias mapping used by `RegisterListenersPass`.
* Made the `event` attribute of the `kernel.event_listener` tag optional for FQCN events.
4.3.0
-----
* The signature of the `EventDispatcherInterface::dispatch()` method should be updated to `dispatch($event, string $eventName = null)`, not doing so is deprecated
* deprecated the `Event` class, use `Symfony\Contracts\EventDispatcher\Event` instead
4.1.0
-----
* added support for invokable event listeners tagged with `kernel.event_listener` by default
* The `TraceableEventDispatcher::getOrphanedEvents()` method has been added.
* The `TraceableEventDispatcherInterface` has been deprecated.
4.0.0
-----
* removed the `ContainerAwareEventDispatcher` class
* added the `reset()` method to the `TraceableEventDispatcherInterface`
3.4.0
-----
* Implementing `TraceableEventDispatcherInterface` without the `reset()` method has been deprecated.
3.3.0
-----
* The ContainerAwareEventDispatcher class has been deprecated. Use EventDispatcher with closure factories instead.
3.0.0
-----
* The method `getListenerPriority($eventName, $listener)` has been added to the
`EventDispatcherInterface`.
* The methods `Event::setDispatcher()`, `Event::getDispatcher()`, `Event::setName()`
and `Event::getName()` have been removed.
The event dispatcher and the event name are passed to the listener call.
2.5.0
-----
* added Debug\TraceableEventDispatcher (originally in HttpKernel)
* changed Debug\TraceableEventDispatcherInterface to extend EventDispatcherInterface
* added RegisterListenersPass (originally in HttpKernel)
2.1.0
-----
* added TraceableEventDispatcherInterface
* added ContainerAwareEventDispatcher
* added a reference to the EventDispatcher on the Event
* added a reference to the Event name on the event
* added fluid interface to the dispatch() method which now returns the Event
object
* added GenericEvent event class
* added the possibility for subscribers to subscribe several times for the
same event
* added ImmutableEventDispatcher

View File

@@ -1,355 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\EventDispatcher\Debug;
use Psr\EventDispatcher\StoppableEventInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Contracts\Service\ResetInterface;
/**
* Collects some data about event listeners.
*
* This event dispatcher delegates the dispatching to another one.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class TraceableEventDispatcher implements EventDispatcherInterface, ResetInterface
{
/**
* @var \SplObjectStorage<WrappedListener, array{string, string}>|null
*/
private ?\SplObjectStorage $callStack = null;
private array $wrappedListeners = [];
private array $orphanedEvents = [];
private string $currentRequestHash = '';
public function __construct(
private EventDispatcherInterface $dispatcher,
protected Stopwatch $stopwatch,
protected ?LoggerInterface $logger = null,
private ?RequestStack $requestStack = null,
protected readonly ?\Closure $disabled = null,
) {
}
public function addListener(string $eventName, callable|array $listener, int $priority = 0): void
{
$this->dispatcher->addListener($eventName, $listener, $priority);
}
public function addSubscriber(EventSubscriberInterface $subscriber): void
{
$this->dispatcher->addSubscriber($subscriber);
}
public function removeListener(string $eventName, callable|array $listener): void
{
if (isset($this->wrappedListeners[$eventName])) {
foreach ($this->wrappedListeners[$eventName] as $index => $wrappedListener) {
if ($wrappedListener->getWrappedListener() === $listener || ($listener instanceof \Closure && $wrappedListener->getWrappedListener() == $listener)) {
$listener = $wrappedListener;
unset($this->wrappedListeners[$eventName][$index]);
break;
}
}
}
$this->dispatcher->removeListener($eventName, $listener);
}
public function removeSubscriber(EventSubscriberInterface $subscriber): void
{
$this->dispatcher->removeSubscriber($subscriber);
}
public function getListeners(?string $eventName = null): array
{
return $this->dispatcher->getListeners($eventName);
}
public function getListenerPriority(string $eventName, callable|array $listener): ?int
{
// we might have wrapped listeners for the event (if called while dispatching)
// in that case get the priority by wrapper
if (isset($this->wrappedListeners[$eventName])) {
foreach ($this->wrappedListeners[$eventName] as $wrappedListener) {
if ($wrappedListener->getWrappedListener() === $listener || ($listener instanceof \Closure && $wrappedListener->getWrappedListener() == $listener)) {
return $this->dispatcher->getListenerPriority($eventName, $wrappedListener);
}
}
}
return $this->dispatcher->getListenerPriority($eventName, $listener);
}
public function hasListeners(?string $eventName = null): bool
{
return $this->dispatcher->hasListeners($eventName);
}
public function dispatch(object $event, ?string $eventName = null): object
{
if ($this->disabled?->__invoke()) {
return $this->dispatcher->dispatch($event, $eventName);
}
$eventName ??= $event::class;
$this->callStack ??= new \SplObjectStorage();
$currentRequestHash = $this->currentRequestHash = $this->requestStack && ($request = $this->requestStack->getCurrentRequest()) ? spl_object_hash($request) : '';
if (null !== $this->logger && $event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
$this->logger->debug(\sprintf('The "%s" event is already stopped. No listeners have been called.', $eventName));
}
$this->preProcess($eventName);
try {
$this->beforeDispatch($eventName, $event);
try {
$e = $this->stopwatch->start($eventName, 'section');
try {
$this->dispatcher->dispatch($event, $eventName);
} finally {
if ($e->isStarted()) {
$e->stop();
}
}
} finally {
$this->afterDispatch($eventName, $event);
}
} finally {
$this->currentRequestHash = $currentRequestHash;
$this->postProcess($eventName);
}
return $event;
}
public function getCalledListeners(?Request $request = null): array
{
if (null === $this->callStack) {
return [];
}
$hash = $request ? spl_object_hash($request) : null;
$called = [];
foreach ($this->callStack as $listener) {
[$eventName, $requestHash] = $this->callStack->getInfo();
if (null === $hash || $hash === $requestHash) {
$called[] = $listener->getInfo($eventName);
}
}
return $called;
}
public function getNotCalledListeners(?Request $request = null): array
{
try {
$allListeners = $this->dispatcher instanceof EventDispatcher ? $this->getListenersWithPriority() : $this->getListenersWithoutPriority();
} catch (\Exception $e) {
$this->logger?->info('An exception was thrown while getting the uncalled listeners.', ['exception' => $e]);
// unable to retrieve the uncalled listeners
return [];
}
$hash = $request ? spl_object_hash($request) : null;
$calledListeners = [];
if (null !== $this->callStack) {
foreach ($this->callStack as $calledListener) {
[, $requestHash] = $this->callStack->getInfo();
if (null === $hash || $hash === $requestHash) {
$calledListeners[] = $calledListener->getWrappedListener();
}
}
}
$notCalled = [];
foreach ($allListeners as $eventName => $listeners) {
foreach ($listeners as [$listener, $priority]) {
if (!\in_array($listener, $calledListeners, true)) {
if (!$listener instanceof WrappedListener) {
$listener = new WrappedListener($listener, null, $this->stopwatch, $this, $priority);
}
$notCalled[] = $listener->getInfo($eventName);
}
}
}
uasort($notCalled, $this->sortNotCalledListeners(...));
return $notCalled;
}
public function getOrphanedEvents(?Request $request = null): array
{
if ($request) {
return $this->orphanedEvents[spl_object_hash($request)] ?? [];
}
if (!$this->orphanedEvents) {
return [];
}
return array_merge(...array_values($this->orphanedEvents));
}
public function reset(): void
{
$this->callStack = null;
$this->orphanedEvents = [];
$this->currentRequestHash = '';
}
/**
* Proxies all method calls to the original event dispatcher.
*
* @param string $method The method name
* @param array $arguments The method arguments
*/
public function __call(string $method, array $arguments): mixed
{
return $this->dispatcher->{$method}(...$arguments);
}
/**
* Called before dispatching the event.
*/
protected function beforeDispatch(string $eventName, object $event): void
{
}
/**
* Called after dispatching the event.
*/
protected function afterDispatch(string $eventName, object $event): void
{
}
private function preProcess(string $eventName): void
{
if (!$this->dispatcher->hasListeners($eventName)) {
$this->orphanedEvents[$this->currentRequestHash][] = $eventName;
return;
}
foreach ($this->dispatcher->getListeners($eventName) as $listener) {
$priority = $this->getListenerPriority($eventName, $listener);
$wrappedListener = new WrappedListener($listener instanceof WrappedListener ? $listener->getWrappedListener() : $listener, null, $this->stopwatch, $this);
$this->wrappedListeners[$eventName][] = $wrappedListener;
$this->dispatcher->removeListener($eventName, $listener);
$this->dispatcher->addListener($eventName, $wrappedListener, $priority);
$this->callStack[$wrappedListener] = [$eventName, $this->currentRequestHash];
}
}
private function postProcess(string $eventName): void
{
unset($this->wrappedListeners[$eventName]);
$skipped = false;
foreach ($this->dispatcher->getListeners($eventName) as $listener) {
if (!$listener instanceof WrappedListener) { // #12845: a new listener was added during dispatch.
continue;
}
// Unwrap listener
$priority = $this->getListenerPriority($eventName, $listener);
$this->dispatcher->removeListener($eventName, $listener);
$this->dispatcher->addListener($eventName, $listener->getWrappedListener(), $priority);
if (null !== $this->logger) {
$context = ['event' => $eventName, 'listener' => $listener->getPretty()];
}
if ($listener->wasCalled()) {
$this->logger?->debug('Notified event "{event}" to listener "{listener}".', $context);
} else {
unset($this->callStack[$listener]);
}
if (null !== $this->logger && $skipped) {
$this->logger->debug('Listener "{listener}" was not called for event "{event}".', $context);
}
if ($listener->stoppedPropagation()) {
$this->logger?->debug('Listener "{listener}" stopped propagation of the event "{event}".', $context);
$skipped = true;
}
}
}
private function sortNotCalledListeners(array $a, array $b): int
{
if (0 !== $cmp = strcmp($a['event'], $b['event'])) {
return $cmp;
}
if (\is_int($a['priority']) && !\is_int($b['priority'])) {
return 1;
}
if (!\is_int($a['priority']) && \is_int($b['priority'])) {
return -1;
}
if ($a['priority'] === $b['priority']) {
return 0;
}
if ($a['priority'] > $b['priority']) {
return -1;
}
return 1;
}
private function getListenersWithPriority(): array
{
$result = [];
$allListeners = new \ReflectionProperty(EventDispatcher::class, 'listeners');
foreach ($allListeners->getValue($this->dispatcher) as $eventName => $listenersByPriority) {
foreach ($listenersByPriority as $priority => $listeners) {
foreach ($listeners as $listener) {
$result[$eventName][] = [$listener, $priority];
}
}
}
return $result;
}
private function getListenersWithoutPriority(): array
{
$result = [];
foreach ($this->getListeners() as $eventName => $listeners) {
foreach ($listeners as $listener) {
$result[$eventName][] = [$listener, null];
}
}
return $result;
}
}

View File

@@ -1,143 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\EventDispatcher\Debug;
use Psr\EventDispatcher\StoppableEventInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Component\VarDumper\Caster\ClassStub;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class WrappedListener
{
private string|array|object $listener;
private ?\Closure $optimizedListener;
private string $name;
private bool $called = false;
private bool $stoppedPropagation = false;
private string $pretty;
private string $callableRef;
private ClassStub|string $stub;
private static bool $hasClassStub;
public function __construct(
callable|array $listener,
?string $name,
private Stopwatch $stopwatch,
private ?EventDispatcherInterface $dispatcher = null,
private ?int $priority = null,
) {
$this->listener = $listener;
$this->optimizedListener = $listener instanceof \Closure ? $listener : (\is_callable($listener) ? $listener(...) : null);
if (\is_array($listener)) {
[$this->name, $this->callableRef] = $this->parseListener($listener);
$this->pretty = $this->name.'::'.$listener[1];
$this->callableRef .= '::'.$listener[1];
} elseif ($listener instanceof \Closure) {
$r = new \ReflectionFunction($listener);
if ($r->isAnonymous()) {
$this->pretty = $this->name = 'closure';
} elseif ($class = $r->getClosureCalledClass()) {
$this->name = $class->name;
$this->pretty = $this->name.'::'.$r->name;
} else {
$this->pretty = $this->name = $r->name;
}
} elseif (\is_string($listener)) {
$this->pretty = $this->name = $listener;
} else {
$this->name = get_debug_type($listener);
$this->pretty = $this->name.'::__invoke';
$this->callableRef = $listener::class.'::__invoke';
}
if (null !== $name) {
$this->name = $name;
}
self::$hasClassStub ??= class_exists(ClassStub::class);
}
public function getWrappedListener(): callable|array
{
return $this->listener;
}
public function wasCalled(): bool
{
return $this->called;
}
public function stoppedPropagation(): bool
{
return $this->stoppedPropagation;
}
public function getPretty(): string
{
return $this->pretty;
}
public function getInfo(string $eventName): array
{
$this->stub ??= self::$hasClassStub ? new ClassStub($this->pretty.'()', $this->callableRef ?? $this->listener) : $this->pretty.'()';
return [
'event' => $eventName,
'priority' => $this->priority ??= $this->dispatcher?->getListenerPriority($eventName, $this->listener),
'pretty' => $this->pretty,
'stub' => $this->stub,
];
}
public function __invoke(object $event, string $eventName, EventDispatcherInterface $dispatcher): void
{
$dispatcher = $this->dispatcher ?: $dispatcher;
$this->called = true;
$this->priority ??= $dispatcher->getListenerPriority($eventName, $this->listener);
$e = $this->stopwatch->start($this->name, 'event_listener');
try {
($this->optimizedListener ?? $this->listener)($event, $eventName, $dispatcher);
} finally {
if ($e->isStarted()) {
$e->stop();
}
}
if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
$this->stoppedPropagation = true;
}
}
private function parseListener(array $listener): array
{
if ($listener[0] instanceof \Closure) {
foreach ((new \ReflectionFunction($listener[0]))->getAttributes(\Closure::class) as $attribute) {
if ($name = $attribute->getArguments()['name'] ?? false) {
return [$name, $attribute->getArguments()['class'] ?? $name];
}
}
}
if (\is_object($listener[0])) {
return [get_debug_type($listener[0]), $listener[0]::class];
}
return [$listener[0], $listener[0]];
}
}

View File

@@ -1,38 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\EventDispatcher\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* This pass allows bundles to extend the list of event aliases.
*
* @author Alexander M. Turek <me@derrabus.de>
*/
class AddEventAliasesPass implements CompilerPassInterface
{
public function __construct(
private array $eventAliases,
) {
}
public function process(ContainerBuilder $container): void
{
$eventAliases = $container->hasParameter('event_dispatcher.event_aliases') ? $container->getParameter('event_dispatcher.event_aliases') : [];
$container->setParameter(
'event_dispatcher.event_aliases',
array_merge($eventAliases, $this->eventAliases)
);
}
}

View File

@@ -1,213 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\EventDispatcher\DependencyInjection;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Contracts\EventDispatcher\Event;
/**
* Compiler pass to register tagged services for an event dispatcher.
*/
class RegisterListenersPass implements CompilerPassInterface
{
private array $hotPathEvents = [];
private array $noPreloadEvents = [];
/**
* @return $this
*/
public function setHotPathEvents(array $hotPathEvents): static
{
$this->hotPathEvents = array_flip($hotPathEvents);
return $this;
}
/**
* @return $this
*/
public function setNoPreloadEvents(array $noPreloadEvents): static
{
$this->noPreloadEvents = array_flip($noPreloadEvents);
return $this;
}
public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition('event_dispatcher') && !$container->hasAlias('event_dispatcher')) {
return;
}
$aliases = [];
if ($container->hasParameter('event_dispatcher.event_aliases')) {
$aliases = $container->getParameter('event_dispatcher.event_aliases');
}
$globalDispatcherDefinition = $container->findDefinition('event_dispatcher');
foreach ($container->findTaggedServiceIds('kernel.event_listener', true) as $id => $events) {
$noPreload = 0;
foreach ($events as $event) {
$priority = $event['priority'] ?? 0;
if (!isset($event['event'])) {
if ($container->getDefinition($id)->hasTag('kernel.event_subscriber')) {
continue;
}
$event['method'] ??= '__invoke';
$event['event'] = $this->getEventFromTypeDeclaration($container, $id, $event['method']);
}
$event['event'] = $aliases[$event['event']] ?? $event['event'];
if (!isset($event['method'])) {
$event['method'] = 'on'.preg_replace_callback([
'/(?<=\b|_)[a-z]/i',
'/[^a-z0-9]/i',
], fn ($matches) => strtoupper($matches[0]), $event['event']);
$event['method'] = preg_replace('/[^a-z0-9]/i', '', $event['method']);
if (null !== ($class = $container->getDefinition($id)->getClass()) && ($r = $container->getReflectionClass($class, false)) && !$r->hasMethod($event['method'])) {
if (!$r->hasMethod('__invoke')) {
throw new InvalidArgumentException(\sprintf('None of the "%s" or "__invoke" methods exist for the service "%s". Please define the "method" attribute on "kernel.event_listener" tags.', $event['method'], $id));
}
$event['method'] = '__invoke';
}
}
$dispatcherDefinition = $globalDispatcherDefinition;
if (isset($event['dispatcher'])) {
$dispatcherDefinition = $container->findDefinition($event['dispatcher']);
}
$dispatcherDefinition->addMethodCall('addListener', [$event['event'], [new ServiceClosureArgument(new Reference($id)), $event['method']], $priority]);
if (isset($this->hotPathEvents[$event['event']])) {
$container->getDefinition($id)->addTag('container.hot_path');
} elseif (isset($this->noPreloadEvents[$event['event']])) {
++$noPreload;
}
}
if ($noPreload && \count($events) === $noPreload) {
$container->getDefinition($id)->addTag('container.no_preload');
}
}
$extractingDispatcher = new ExtractingEventDispatcher();
foreach ($container->findTaggedServiceIds('kernel.event_subscriber', true) as $id => $tags) {
$def = $container->getDefinition($id);
// We must assume that the class value has been correctly filled, even if the service is created by a factory
$class = $def->getClass();
if (!$r = $container->getReflectionClass($class)) {
throw new InvalidArgumentException(\sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
}
if (!$r->isSubclassOf(EventSubscriberInterface::class)) {
throw new InvalidArgumentException(\sprintf('Service "%s" must implement interface "%s".', $id, EventSubscriberInterface::class));
}
$class = $r->name;
$dispatcherDefinitions = [];
foreach ($tags as $attributes) {
if (!isset($attributes['dispatcher']) || isset($dispatcherDefinitions[$attributes['dispatcher']])) {
continue;
}
$dispatcherDefinitions[$attributes['dispatcher']] = $container->findDefinition($attributes['dispatcher']);
}
if (!$dispatcherDefinitions) {
$dispatcherDefinitions = [$globalDispatcherDefinition];
}
$noPreload = 0;
ExtractingEventDispatcher::$aliases = $aliases;
ExtractingEventDispatcher::$subscriber = $class;
$extractingDispatcher->addSubscriber($extractingDispatcher);
foreach ($extractingDispatcher->listeners as $args) {
$args[1] = [new ServiceClosureArgument(new Reference($id)), $args[1]];
foreach ($dispatcherDefinitions as $dispatcherDefinition) {
$dispatcherDefinition->addMethodCall('addListener', $args);
}
if (isset($this->hotPathEvents[$args[0]])) {
$container->getDefinition($id)->addTag('container.hot_path');
} elseif (isset($this->noPreloadEvents[$args[0]])) {
++$noPreload;
}
}
if ($noPreload && \count($extractingDispatcher->listeners) === $noPreload) {
$container->getDefinition($id)->addTag('container.no_preload');
}
$extractingDispatcher->listeners = [];
ExtractingEventDispatcher::$aliases = [];
}
}
private function getEventFromTypeDeclaration(ContainerBuilder $container, string $id, string $method): string
{
if (
null === ($class = $container->getDefinition($id)->getClass())
|| !($r = $container->getReflectionClass($class, false))
|| !$r->hasMethod($method)
|| 1 > ($m = $r->getMethod($method))->getNumberOfParameters()
|| !($type = $m->getParameters()[0]->getType()) instanceof \ReflectionNamedType
|| $type->isBuiltin()
|| Event::class === ($name = $type->getName())
) {
throw new InvalidArgumentException(\sprintf('Service "%s" must define the "event" attribute on "kernel.event_listener" tags.', $id));
}
return $name;
}
}
/**
* @internal
*/
class ExtractingEventDispatcher extends EventDispatcher implements EventSubscriberInterface
{
public array $listeners = [];
public static array $aliases = [];
public static string $subscriber;
public function addListener(string $eventName, callable|array $listener, int $priority = 0): void
{
$this->listeners[] = [$eventName, $listener[1], $priority];
}
public static function getSubscribedEvents(): array
{
$events = [];
foreach ([self::$subscriber, 'getSubscribedEvents']() as $eventName => $params) {
$events[self::$aliases[$eventName] ?? $eventName] = $params;
}
return $events;
}
}

View File

@@ -1,256 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\EventDispatcher;
use Psr\EventDispatcher\StoppableEventInterface;
use Symfony\Component\EventDispatcher\Debug\WrappedListener;
/**
* The EventDispatcherInterface is the central point of Symfony's event listener system.
*
* Listeners are registered on the manager and events are dispatched through the
* manager.
*
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @author Jordan Alliot <jordan.alliot@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
class EventDispatcher implements EventDispatcherInterface
{
private array $listeners = [];
private array $sorted = [];
private array $optimized;
public function __construct()
{
if (__CLASS__ === static::class) {
$this->optimized = [];
}
}
public function dispatch(object $event, ?string $eventName = null): object
{
$eventName ??= $event::class;
if (isset($this->optimized)) {
$listeners = $this->optimized[$eventName] ?? (empty($this->listeners[$eventName]) ? [] : $this->optimizeListeners($eventName));
} else {
$listeners = $this->getListeners($eventName);
}
if ($listeners) {
$this->callListeners($listeners, $eventName, $event);
}
return $event;
}
public function getListeners(?string $eventName = null): array
{
if (null !== $eventName) {
if (empty($this->listeners[$eventName])) {
return [];
}
if (!isset($this->sorted[$eventName])) {
$this->sortListeners($eventName);
}
return $this->sorted[$eventName];
}
foreach ($this->listeners as $eventName => $eventListeners) {
if (!isset($this->sorted[$eventName])) {
$this->sortListeners($eventName);
}
}
return array_filter($this->sorted);
}
public function getListenerPriority(string $eventName, callable|array $listener): ?int
{
if (empty($this->listeners[$eventName])) {
return null;
}
if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) {
$listener[0] = $listener[0]();
$listener[1] ??= '__invoke';
}
foreach ($this->listeners[$eventName] as $priority => &$listeners) {
foreach ($listeners as &$v) {
if ($v !== $listener && \is_array($v) && isset($v[0]) && $v[0] instanceof \Closure && 2 >= \count($v)) {
$v[0] = $v[0]();
$v[1] ??= '__invoke';
}
if ($v === $listener || ($listener instanceof \Closure && $v == $listener)) {
return $priority;
}
}
}
return null;
}
public function hasListeners(?string $eventName = null): bool
{
if (null !== $eventName) {
return !empty($this->listeners[$eventName]);
}
foreach ($this->listeners as $eventListeners) {
if ($eventListeners) {
return true;
}
}
return false;
}
public function addListener(string $eventName, callable|array $listener, int $priority = 0): void
{
$this->listeners[$eventName][$priority][] = $listener;
unset($this->sorted[$eventName], $this->optimized[$eventName]);
}
public function removeListener(string $eventName, callable|array $listener): void
{
if (empty($this->listeners[$eventName])) {
return;
}
if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) {
$listener[0] = $listener[0]();
$listener[1] ??= '__invoke';
}
foreach ($this->listeners[$eventName] as $priority => &$listeners) {
foreach ($listeners as $k => &$v) {
if ($v !== $listener && \is_array($v) && isset($v[0]) && $v[0] instanceof \Closure && 2 >= \count($v)) {
$v[0] = $v[0]();
$v[1] ??= '__invoke';
}
if ($v === $listener || ($listener instanceof \Closure && $v == $listener)) {
unset($listeners[$k], $this->sorted[$eventName], $this->optimized[$eventName]);
}
}
if (!$listeners) {
unset($this->listeners[$eventName][$priority]);
}
}
}
public function addSubscriber(EventSubscriberInterface $subscriber): void
{
foreach ($subscriber->getSubscribedEvents() as $eventName => $params) {
if (\is_string($params)) {
$this->addListener($eventName, [$subscriber, $params]);
} elseif (\is_string($params[0])) {
$this->addListener($eventName, [$subscriber, $params[0]], $params[1] ?? 0);
} else {
foreach ($params as $listener) {
$this->addListener($eventName, [$subscriber, $listener[0]], $listener[1] ?? 0);
}
}
}
}
public function removeSubscriber(EventSubscriberInterface $subscriber): void
{
foreach ($subscriber->getSubscribedEvents() as $eventName => $params) {
if (\is_array($params) && \is_array($params[0])) {
foreach ($params as $listener) {
$this->removeListener($eventName, [$subscriber, $listener[0]]);
}
} else {
$this->removeListener($eventName, [$subscriber, \is_string($params) ? $params : $params[0]]);
}
}
}
/**
* Triggers the listeners of an event.
*
* This method can be overridden to add functionality that is executed
* for each listener.
*
* @param callable[] $listeners The event listeners
* @param string $eventName The name of the event to dispatch
* @param object $event The event object to pass to the event handlers/listeners
*/
protected function callListeners(iterable $listeners, string $eventName, object $event): void
{
$stoppable = $event instanceof StoppableEventInterface;
foreach ($listeners as $listener) {
if ($stoppable && $event->isPropagationStopped()) {
break;
}
$listener($event, $eventName, $this);
}
}
/**
* Sorts the internal list of listeners for the given event by priority.
*/
private function sortListeners(string $eventName): void
{
krsort($this->listeners[$eventName]);
$this->sorted[$eventName] = [];
foreach ($this->listeners[$eventName] as &$listeners) {
foreach ($listeners as &$listener) {
if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) {
$listener[0] = $listener[0]();
$listener[1] ??= '__invoke';
}
$this->sorted[$eventName][] = $listener;
}
}
}
/**
* Optimizes the internal list of listeners for the given event by priority.
*/
private function optimizeListeners(string $eventName): array
{
krsort($this->listeners[$eventName]);
$this->optimized[$eventName] = [];
foreach ($this->listeners[$eventName] as &$listeners) {
foreach ($listeners as &$listener) {
$closure = &$this->optimized[$eventName][];
if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) {
$closure = static function (...$args) use (&$listener, &$closure) {
if ($listener[0] instanceof \Closure) {
$listener[0] = $listener[0]();
$listener[1] ??= '__invoke';
}
($closure = $listener(...))(...$args);
};
} else {
$closure = $listener instanceof WrappedListener ? $listener : $listener(...);
}
}
}
return $this->optimized[$eventName];
}
}

View File

@@ -1,66 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\EventDispatcher;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as ContractsEventDispatcherInterface;
/**
* The EventDispatcherInterface is the central point of Symfony's event listener system.
* Listeners are registered on the manager and events are dispatched through the
* manager.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface EventDispatcherInterface extends ContractsEventDispatcherInterface
{
/**
* Adds an event listener that listens on the specified events.
*
* @param int $priority The higher this value, the earlier an event
* listener will be triggered in the chain (defaults to 0)
*/
public function addListener(string $eventName, callable $listener, int $priority = 0): void;
/**
* Adds an event subscriber.
*
* The subscriber is asked for all the events it is
* interested in and added as a listener for these events.
*/
public function addSubscriber(EventSubscriberInterface $subscriber): void;
/**
* Removes an event listener from the specified events.
*/
public function removeListener(string $eventName, callable $listener): void;
public function removeSubscriber(EventSubscriberInterface $subscriber): void;
/**
* Gets the listeners of a specific event or all listeners sorted by descending priority.
*
* @return array<callable[]|callable>
*/
public function getListeners(?string $eventName = null): array;
/**
* Gets the listener priority for a specific event.
*
* Returns null if the event or the listener does not exist.
*/
public function getListenerPriority(string $eventName, callable $listener): ?int;
/**
* Checks whether an event has any registered listeners.
*/
public function hasListeners(?string $eventName = null): bool;
}

View File

@@ -1,49 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\EventDispatcher;
/**
* An EventSubscriber knows itself what events it is interested in.
* If an EventSubscriber is added to an EventDispatcherInterface, the manager invokes
* {@link getSubscribedEvents} and registers the subscriber as a listener for all
* returned events.
*
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface EventSubscriberInterface
{
/**
* Returns an array of event names this subscriber wants to listen to.
*
* The array keys are event names and the value can be:
*
* * The method name to call (priority defaults to 0)
* * An array composed of the method name to call and the priority
* * An array of arrays composed of the method names to call and respective
* priorities, or 0 if unset
*
* For instance:
*
* * ['eventName' => 'methodName']
* * ['eventName' => ['methodName', $priority]]
* * ['eventName' => [['methodName1', $priority], ['methodName2']]]
*
* The code must not depend on runtime state as it will only be called at compile time.
* All logic depending on runtime state must be put into the individual methods handling the events.
*
* @return array<string, string|array{0: string, 1: int}|list<array{0: string, 1?: int}>>
*/
public static function getSubscribedEvents();
}

View File

@@ -1,155 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\EventDispatcher;
use Symfony\Contracts\EventDispatcher\Event;
/**
* Event encapsulation class.
*
* Encapsulates events thus decoupling the observer from the subject they encapsulate.
*
* @author Drak <drak@zikula.org>
*
* @implements \ArrayAccess<string, mixed>
* @implements \IteratorAggregate<string, mixed>
*/
class GenericEvent extends Event implements \ArrayAccess, \IteratorAggregate
{
/**
* Encapsulate an event with $subject and $arguments.
*
* @param mixed $subject The subject of the event, usually an object or a callable
* @param array $arguments Arguments to store in the event
*/
public function __construct(
protected mixed $subject = null,
protected array $arguments = [],
) {
}
/**
* Getter for subject property.
*/
public function getSubject(): mixed
{
return $this->subject;
}
/**
* Get argument by key.
*
* @throws \InvalidArgumentException if key is not found
*/
public function getArgument(string $key): mixed
{
if ($this->hasArgument($key)) {
return $this->arguments[$key];
}
throw new \InvalidArgumentException(\sprintf('Argument "%s" not found.', $key));
}
/**
* Add argument to event.
*
* @return $this
*/
public function setArgument(string $key, mixed $value): static
{
$this->arguments[$key] = $value;
return $this;
}
/**
* Getter for all arguments.
*/
public function getArguments(): array
{
return $this->arguments;
}
/**
* Set args property.
*
* @return $this
*/
public function setArguments(array $args = []): static
{
$this->arguments = $args;
return $this;
}
/**
* Has argument.
*/
public function hasArgument(string $key): bool
{
return \array_key_exists($key, $this->arguments);
}
/**
* ArrayAccess for argument getter.
*
* @param string $key Array key
*
* @throws \InvalidArgumentException if key does not exist in $this->args
*/
public function offsetGet(mixed $key): mixed
{
return $this->getArgument($key);
}
/**
* ArrayAccess for argument setter.
*
* @param string $key Array key to set
*/
public function offsetSet(mixed $key, mixed $value): void
{
$this->setArgument($key, $value);
}
/**
* ArrayAccess for unset argument.
*
* @param string $key Array key
*/
public function offsetUnset(mixed $key): void
{
if ($this->hasArgument($key)) {
unset($this->arguments[$key]);
}
}
/**
* ArrayAccess has argument.
*
* @param string $key Array key
*/
public function offsetExists(mixed $key): bool
{
return $this->hasArgument($key);
}
/**
* IteratorAggregate for iterating over the object like an array.
*
* @return \ArrayIterator<string, mixed>
*/
public function getIterator(): \ArrayIterator
{
return new \ArrayIterator($this->arguments);
}
}

View File

@@ -1,65 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\EventDispatcher;
/**
* A read-only proxy for an event dispatcher.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ImmutableEventDispatcher implements EventDispatcherInterface
{
public function __construct(
private EventDispatcherInterface $dispatcher,
) {
}
public function dispatch(object $event, ?string $eventName = null): object
{
return $this->dispatcher->dispatch($event, $eventName);
}
public function addListener(string $eventName, callable|array $listener, int $priority = 0): never
{
throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.');
}
public function addSubscriber(EventSubscriberInterface $subscriber): never
{
throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.');
}
public function removeListener(string $eventName, callable|array $listener): never
{
throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.');
}
public function removeSubscriber(EventSubscriberInterface $subscriber): never
{
throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.');
}
public function getListeners(?string $eventName = null): array
{
return $this->dispatcher->getListeners($eventName);
}
public function getListenerPriority(string $eventName, callable|array $listener): ?int
{
return $this->dispatcher->getListenerPriority($eventName, $listener);
}
public function hasListeners(?string $eventName = null): bool
{
return $this->dispatcher->hasListeners($eventName);
}
}

View File

@@ -1,19 +0,0 @@
Copyright (c) 2004-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -1,15 +0,0 @@
EventDispatcher Component
=========================
The EventDispatcher component provides tools that allow your application
components to communicate with each other by dispatching events and listening to
them.
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/event_dispatcher.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)

View File

@@ -1,47 +0,0 @@
{
"name": "symfony/event-dispatcher",
"type": "library",
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
"keywords": [],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=8.2",
"symfony/event-dispatcher-contracts": "^2.5|^3"
},
"require-dev": {
"symfony/dependency-injection": "^6.4|^7.0",
"symfony/expression-language": "^6.4|^7.0",
"symfony/config": "^6.4|^7.0",
"symfony/error-handler": "^6.4|^7.0",
"symfony/http-foundation": "^6.4|^7.0",
"symfony/service-contracts": "^2.5|^3",
"symfony/stopwatch": "^6.4|^7.0",
"psr/log": "^1|^2|^3"
},
"conflict": {
"symfony/dependency-injection": "<6.4",
"symfony/service-contracts": "<2.5"
},
"provide": {
"psr/event-dispatcher-implementation": "1.0",
"symfony/event-dispatcher-implementation": "2.0|3.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\EventDispatcher\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev"
}

View File

@@ -1,130 +0,0 @@
CHANGELOG
=========
7.3
---
* Add DSN param `retry_period` to override default email transport retry period
* Add `Dsn::getBooleanOption()`
* Add DSN param `source_ip` to allow binding to a (specific) IPv4 or IPv6 address.
* Add DSN param `require_tls` to enforce use of TLS/STARTTLS
* Add `DkimSignedMessageListener`, `SmimeEncryptedMessageListener`, and `SmimeSignedMessageListener`
7.2
---
* Deprecate `TransportFactoryTestCase`, extend `AbstractTransportFactoryTestCase` instead
The `testIncompleteDsnException()` test is no longer provided by default. If you make use of it by implementing the `incompleteDsnProvider()` data providers,
you now need to use the `IncompleteDsnTestTrait`.
* Make `TransportFactoryTestCase` compatible with PHPUnit 10+
* Support unicode email addresses such as "dømi@dømi.example"
7.1
---
* Dispatch Postmark's "406 - Inactive recipient" API error code as a `PostmarkDeliveryEvent` instead of throwing an exception
* Add DSN param `auto_tls` to disable automatic STARTTLS
* Add support for allowing some users even if `recipients` is defined in `EnvelopeListener`
7.0
---
* Remove the OhMySmtp bridge in favor of the MailPace bridge
6.4
---
* Add DSN parameter `peer_fingerprint` to verify TLS certificate fingerprint
* Change the default port for the `mailjet+smtp` transport from 465 to 587
6.3
---
* Add `MessageEvent::reject()` to allow rejecting an email before sending it
* Change the default port for the `mailgun+smtp` transport from 465 to 587
* Add `$authenticators` parameter in `EsmtpTransport` constructor and `EsmtpTransport::setAuthenticators()`
to allow overriding of default eSMTP authenticators
6.2.7
-----
* [BC BREAK] The following data providers for `TransportFactoryTestCase` are now static:
`supportsProvider()`, `createProvider()`, `unsupportedSchemeProvider()`and `incompleteDsnProvider()`
6.2
---
* Add a `mailer:test` command
* Add `SentMessageEvent` and `FailedMessageEvent` events
6.1
---
* Make `start()` and `stop()` methods public on `SmtpTransport`
* Improve extensibility of `EsmtpTransport`
6.0
---
* The `HttpTransportException` class takes a string at first argument
5.4
---
* Enable the mailer to operate on any PSR-14-compatible event dispatcher
5.3
---
* added the `mailer` monolog channel and set it on all transport definitions
5.2.0
-----
* added `NativeTransportFactory` to configure a transport based on php.ini settings
* added `local_domain`, `restart_threshold`, `restart_threshold_sleep` and `ping_threshold` options for `smtp`
* added `command` option for `sendmail`
4.4.0
-----
* [BC BREAK] changed the `NullTransport` DSN from `smtp://null` to `null://null`
* [BC BREAK] renamed `SmtpEnvelope` to `Envelope`, renamed `DelayedSmtpEnvelope` to
`DelayedEnvelope`
* [BC BREAK] changed the syntax for failover and roundrobin DSNs
Before:
dummy://a || dummy://b (for failover)
dummy://a && dummy://b (for roundrobin)
After:
failover(dummy://a dummy://b)
roundrobin(dummy://a dummy://b)
* added support for multiple transports on a `Mailer` instance
* [BC BREAK] removed the `auth_mode` DSN option (it is now always determined automatically)
* STARTTLS cannot be enabled anymore (it is used automatically if TLS is disabled and the server supports STARTTLS)
* [BC BREAK] Removed the `encryption` DSN option (use `smtps` instead)
* Added support for the `smtps` protocol (does the same as using `smtp` and port `465`)
* Added PHPUnit constraints
* Added `MessageDataCollector`
* Added `MessageEvents` and `MessageLoggerListener` to allow collecting sent emails
* [BC BREAK] `TransportInterface` has a new `__toString()` method
* [BC BREAK] Classes `AbstractApiTransport` and `AbstractHttpTransport` moved under `Transport` sub-namespace.
* [BC BREAK] Transports depend on `Symfony\Contracts\EventDispatcher\EventDispatcherInterface`
instead of `Symfony\Component\EventDispatcher\EventDispatcherInterface`.
* Added possibility to register custom transport for dsn by implementing
`Symfony\Component\Mailer\Transport\TransportFactoryInterface` and tagging with `mailer.transport_factory` tag in DI.
* Added `Symfony\Component\Mailer\Test\TransportFactoryTestCase` to ease testing custom transport factories.
* Added `SentMessage::getDebug()` and `TransportExceptionInterface::getDebug` to help debugging
* Made `MessageEvent` final
* add DSN parameter `verify_peer` to disable TLS peer verification for SMTP transport
4.3.0
-----
* Added the component.

View File

@@ -1,73 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Mailer\Transport\TransportInterface;
use Symfony\Component\Mime\Email;
/**
* A console command to test Mailer transports.
*/
#[AsCommand(name: 'mailer:test', description: 'Test Mailer transports by sending an email')]
final class MailerTestCommand extends Command
{
public function __construct(private TransportInterface $transport)
{
parent::__construct();
}
protected function configure(): void
{
$this
->addArgument('to', InputArgument::REQUIRED, 'The recipient of the message')
->addOption('from', null, InputOption::VALUE_REQUIRED, 'The sender of the message', 'from@example.org')
->addOption('subject', null, InputOption::VALUE_REQUIRED, 'The subject of the message', 'Testing transport')
->addOption('body', null, InputOption::VALUE_REQUIRED, 'The body of the message', 'Testing body')
->addOption('transport', null, InputOption::VALUE_REQUIRED, 'The transport to be used')
->setHelp(<<<'EOF'
The <info>%command.name%</info> command tests a Mailer transport by sending a simple email message:
<info>php %command.full_name% to@example.com</info>
You can also specify a specific transport:
<info>php %command.full_name% to@example.com --transport=transport_name</info>
Note that this command bypasses the Messenger bus if configured.
EOF
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$message = (new Email())
->to($input->getArgument('to'))
->from($input->getOption('from'))
->subject($input->getOption('subject'))
->text($input->getOption('body'))
;
if ($transport = $input->getOption('transport')) {
$message->getHeaders()->addTextHeader('X-Transport', $transport);
}
$this->transport->send($message);
return 0;
}
}

View File

@@ -1,59 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\DataCollector;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\Mailer\Event\MessageEvents;
use Symfony\Component\Mailer\EventListener\MessageLoggerListener;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class MessageDataCollector extends DataCollector
{
private MessageEvents $events;
public function __construct(MessageLoggerListener $logger)
{
$this->events = $logger->getEvents();
}
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
{
$this->data['events'] = $this->events;
}
public function getEvents(): MessageEvents
{
return $this->data['events'];
}
/**
* @internal
*/
public function base64Encode(string $data): string
{
return base64_encode($data);
}
public function reset(): void
{
$this->data = [];
}
public function getName(): string
{
return 'mailer';
}
}

View File

@@ -1,97 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer;
use Symfony\Component\Mailer\Exception\LogicException;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Header\Headers;
use Symfony\Component\Mime\Message;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
final class DelayedEnvelope extends Envelope
{
private bool $senderSet = false;
private bool $recipientsSet = false;
public function __construct(
private Message $message,
) {
}
public function setSender(Address $sender): void
{
parent::setSender($sender);
$this->senderSet = true;
}
public function getSender(): Address
{
if (!$this->senderSet) {
parent::setSender(self::getSenderFromHeaders($this->message->getHeaders()));
}
return parent::getSender();
}
public function setRecipients(array $recipients): void
{
parent::setRecipients($recipients);
$this->recipientsSet = (bool) parent::getRecipients();
}
/**
* @return Address[]
*/
public function getRecipients(): array
{
if ($this->recipientsSet) {
return parent::getRecipients();
}
return self::getRecipientsFromHeaders($this->message->getHeaders());
}
private static function getRecipientsFromHeaders(Headers $headers): array
{
$recipients = [];
foreach (['to', 'cc', 'bcc'] as $name) {
foreach ($headers->all($name) as $header) {
foreach ($header->getAddresses() as $address) {
$recipients[] = $address;
}
}
}
return $recipients;
}
private static function getSenderFromHeaders(Headers $headers): Address
{
if ($sender = $headers->get('Sender')) {
return $sender->getAddress();
}
if ($return = $headers->get('Return-Path')) {
return $return->getAddress();
}
if ($from = $headers->get('From')) {
return $from->getAddresses()[0];
}
throw new LogicException('Unable to determine the sender of the message.');
}
}

View File

@@ -1,117 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer;
use Symfony\Component\Mailer\Exception\InvalidArgumentException;
use Symfony\Component\Mailer\Exception\LogicException;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\RawMessage;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class Envelope
{
private Address $sender;
private array $recipients = [];
/**
* @param Address[] $recipients
*/
public function __construct(Address $sender, array $recipients)
{
$this->setSender($sender);
$this->setRecipients($recipients);
}
public static function create(RawMessage $message): self
{
if (RawMessage::class === $message::class) {
throw new LogicException('Cannot send a RawMessage instance without an explicit Envelope.');
}
return new DelayedEnvelope($message);
}
public function setSender(Address $sender): void
{
// to ensure deliverability of bounce emails independent of UTF-8 capabilities of SMTP servers
if (!preg_match('/^[^@\x80-\xFF]++@/', $sender->getAddress())) {
throw new InvalidArgumentException(\sprintf('Invalid sender "%s": non-ASCII characters not supported in local-part of email.', $sender->getAddress()));
}
$this->sender = $sender;
}
/**
* @return Address Returns a "mailbox" as specified by RFC 2822
* Must be converted to an "addr-spec" when used as a "MAIL FROM" value in SMTP (use getAddress())
*/
public function getSender(): Address
{
return $this->sender;
}
/**
* @param Address[] $recipients
*/
public function setRecipients(array $recipients): void
{
if (!$recipients) {
throw new InvalidArgumentException('An envelope must have at least one recipient.');
}
$this->recipients = [];
foreach ($recipients as $recipient) {
if (!$recipient instanceof Address) {
throw new InvalidArgumentException(\sprintf('A recipient must be an instance of "%s" (got "%s").', Address::class, get_debug_type($recipient)));
}
$this->recipients[] = new Address($recipient->getAddress());
}
}
/**
* @return Address[]
*/
public function getRecipients(): array
{
return $this->recipients;
}
/**
* Returns true if any address' localpart contains at least one
* non-ASCII character, and false if all addresses have all-ASCII
* localparts.
*
* This helps to decide whether to the SMTPUTF8 extensions (RFC
* 6530 and following) for any given message.
*
* The SMTPUTF8 extension is strictly required if any address
* contains a non-ASCII character in its localpart. If non-ASCII
* is only used in domains (e.g. horst@freiherr-von-mühlhausen.de)
* then it is possible to send the message using IDN encoding
* instead of SMTPUTF8. The most common software will display the
* message as intended.
*/
public function anyAddressHasUnicodeLocalpart(): bool
{
if ($this->getSender()->hasUnicodeLocalpart()) {
return true;
}
foreach ($this->getRecipients() as $r) {
if ($r->hasUnicodeLocalpart()) {
return true;
}
}
return false;
}
}

View File

@@ -1,37 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Event;
use Symfony\Component\Mime\RawMessage;
use Symfony\Contracts\EventDispatcher\Event;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class FailedMessageEvent extends Event
{
public function __construct(
private RawMessage $message,
private \Throwable $error,
) {
}
public function getMessage(): RawMessage
{
return $this->message;
}
public function getError(): \Throwable
{
return $this->error;
}
}

View File

@@ -1,101 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Event;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Exception\LogicException;
use Symfony\Component\Messenger\Stamp\StampInterface;
use Symfony\Component\Mime\RawMessage;
use Symfony\Contracts\EventDispatcher\Event;
/**
* Allows the transformation of a Message and the Envelope before the email is sent.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class MessageEvent extends Event
{
private bool $rejected = false;
/** @var StampInterface[] */
private array $stamps = [];
public function __construct(
private RawMessage $message,
private Envelope $envelope,
private string $transport,
private bool $queued = false,
) {
}
public function getMessage(): RawMessage
{
return $this->message;
}
public function setMessage(RawMessage $message): void
{
$this->message = $message;
}
public function getEnvelope(): Envelope
{
return $this->envelope;
}
public function setEnvelope(Envelope $envelope): void
{
$this->envelope = $envelope;
}
public function getTransport(): string
{
return $this->transport;
}
public function isQueued(): bool
{
return $this->queued;
}
public function isRejected(): bool
{
return $this->rejected;
}
public function reject(): void
{
$this->rejected = true;
$this->stopPropagation();
}
public function addStamp(StampInterface $stamp): void
{
if (!$this->queued) {
throw new LogicException(\sprintf('Cannot call "%s()" on a message that is not meant to be queued.', __METHOD__));
}
$this->stamps[] = $stamp;
}
/**
* @return StampInterface[]
*/
public function getStamps(): array
{
if (!$this->queued) {
throw new LogicException(\sprintf('Cannot call "%s()" on a message that is not meant to be queued.', __METHOD__));
}
return $this->stamps;
}
}

View File

@@ -1,74 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Event;
use Symfony\Component\Mime\RawMessage;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class MessageEvents
{
/**
* @var MessageEvent[]
*/
private array $events = [];
/**
* @var array<string, bool>
*/
private array $transports = [];
public function add(MessageEvent $event): void
{
$this->events[] = $event;
$this->transports[$event->getTransport()] = 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->getTransport()) {
$events[] = $event;
}
}
return $events;
}
/**
* @return RawMessage[]
*/
public function getMessages(?string $name = null): array
{
$events = $this->getEvents($name);
$messages = [];
foreach ($events as $event) {
$messages[] = $event->getMessage();
}
return $messages;
}
}

View File

@@ -1,30 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Event;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Contracts\EventDispatcher\Event;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class SentMessageEvent extends Event
{
public function __construct(private SentMessage $message)
{
}
public function getMessage(): SentMessage
{
return $this->message;
}
}

View File

@@ -1,46 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Mime\Crypto\DkimSigner;
use Symfony\Component\Mime\Message;
/**
* Signs messages using DKIM.
*
* @author Elías Fernández
*/
class DkimSignedMessageListener implements EventSubscriberInterface
{
public function __construct(
private DkimSigner $signer,
) {
}
public function onMessage(MessageEvent $event): void
{
$message = $event->getMessage();
if (!$message instanceof Message) {
return;
}
$event->setMessage($this->signer->sign($message));
}
public static function getSubscribedEvents(): array
{
return [
MessageEvent::class => ['onMessage', -128],
];
}
}

View File

@@ -1,96 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Mime\Address;
use Symfony\Component\Mime\Message;
/**
* Manipulates the Envelope of a Message.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class EnvelopeListener implements EventSubscriberInterface
{
private ?Address $sender = null;
/**
* @var Address[]|null
*/
private ?array $recipients = null;
/**
* @param array<Address|string> $recipients
* @param string[] $allowedRecipients An array of regex to match the allowed recipients
*/
public function __construct(
Address|string|null $sender = null,
?array $recipients = null,
private array $allowedRecipients = [],
) {
if (null !== $sender) {
$this->sender = Address::create($sender);
}
if (null !== $recipients) {
$this->recipients = Address::createArray($recipients);
}
}
public function onMessage(MessageEvent $event): void
{
if ($this->sender) {
$event->getEnvelope()->setSender($this->sender);
$message = $event->getMessage();
if ($message instanceof Message) {
if (!$message->getHeaders()->has('Sender') && !$message->getHeaders()->has('From')) {
$message->getHeaders()->addMailboxHeader('Sender', $this->sender);
}
}
}
if ($this->recipients) {
$recipients = $this->recipients;
if ($this->allowedRecipients) {
foreach ($event->getEnvelope()->getRecipients() as $recipient) {
foreach ($this->allowedRecipients as $allowedRecipient) {
if (!preg_match('{\A'.$allowedRecipient.'\z}', $recipient->getAddress())) {
continue;
}
// dedup
foreach ($recipients as $r) {
if ($r->getName() === $recipient->getName() && $r->getAddress() === $recipient->getAddress()) {
continue 2;
}
}
$recipients[] = $recipient;
continue 2;
}
}
}
$event->getEnvelope()->setRecipients($recipients);
}
}
public static function getSubscribedEvents(): array
{
return [
// should be the last one to allow header changes by other listeners first
MessageEvent::class => ['onMessage', -255],
];
}
}

View File

@@ -1,133 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Exception\InvalidArgumentException;
use Symfony\Component\Mailer\Exception\RuntimeException;
use Symfony\Component\Mime\BodyRendererInterface;
use Symfony\Component\Mime\Header\Headers;
use Symfony\Component\Mime\Header\MailboxListHeader;
use Symfony\Component\Mime\Message;
/**
* Manipulates the headers and the body of a Message.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class MessageListener implements EventSubscriberInterface
{
public const HEADER_SET_IF_EMPTY = 1;
public const HEADER_ADD = 2;
public const HEADER_REPLACE = 3;
public const DEFAULT_RULES = [
'from' => self::HEADER_SET_IF_EMPTY,
'return-path' => self::HEADER_SET_IF_EMPTY,
'reply-to' => self::HEADER_ADD,
'to' => self::HEADER_SET_IF_EMPTY,
'cc' => self::HEADER_ADD,
'bcc' => self::HEADER_ADD,
];
private array $headerRules = [];
public function __construct(
private ?Headers $headers = null,
private ?BodyRendererInterface $renderer = null,
array $headerRules = self::DEFAULT_RULES,
) {
foreach ($headerRules as $headerName => $rule) {
$this->addHeaderRule($headerName, $rule);
}
}
public function addHeaderRule(string $headerName, int $rule): void
{
if ($rule < 1 || $rule > 3) {
throw new InvalidArgumentException(\sprintf('The "%d" rule is not supported.', $rule));
}
$this->headerRules[strtolower($headerName)] = $rule;
}
public function onMessage(MessageEvent $event): void
{
$message = $event->getMessage();
if (!$message instanceof Message) {
return;
}
$this->setHeaders($message);
$this->renderMessage($message);
}
private function setHeaders(Message $message): void
{
if (!$this->headers) {
return;
}
$headers = $message->getHeaders();
foreach ($this->headers->all() as $name => $header) {
if (!$headers->has($name)) {
$headers->add($header);
continue;
}
switch ($this->headerRules[$name] ?? self::HEADER_SET_IF_EMPTY) {
case self::HEADER_SET_IF_EMPTY:
break;
case self::HEADER_REPLACE:
$headers->remove($name);
$headers->add($header);
break;
case self::HEADER_ADD:
if (!Headers::isUniqueHeader($name)) {
$headers->add($header);
break;
}
$h = $headers->get($name);
if (!$h instanceof MailboxListHeader) {
throw new RuntimeException(\sprintf('Unable to set header "%s".', $name));
}
Headers::checkHeaderClass($header);
foreach ($header->getAddresses() as $address) {
$h->addAddress($address);
}
}
}
}
private function renderMessage(Message $message): void
{
if (!$this->renderer) {
return;
}
$this->renderer->render($message);
}
public static function getSubscribedEvents(): array
{
return [
MessageEvent::class => 'onMessage',
];
}
}

View File

@@ -1,54 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <fabien@symfony.com>
*/
class MessageLoggerListener implements EventSubscriberInterface, ResetInterface
{
private MessageEvents $events;
public function __construct()
{
$this->events = new MessageEvents();
}
public function reset(): void
{
$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(): array
{
return [
MessageEvent::class => ['onMessage', -255],
];
}
}

View File

@@ -1,49 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Messenger\Stamp\TransportNamesStamp;
use Symfony\Component\Mime\Message;
/**
* Allows messages to be sent to specific Messenger transports via the "X-Bus-Transport" MIME header.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class MessengerTransportListener implements EventSubscriberInterface
{
public function onMessage(MessageEvent $event): void
{
if (!$event->isQueued()) {
return;
}
$message = $event->getMessage();
if (!$message instanceof Message || !$message->getHeaders()->has('X-Bus-Transport')) {
return;
}
$names = $message->getHeaders()->get('X-Bus-Transport')->getBody();
$names = array_map('trim', explode(',', $names));
$event->addStamp(new TransportNamesStamp($names));
$message->getHeaders()->remove('X-Bus-Transport');
}
public static function getSubscribedEvents(): array
{
return [
MessageEvent::class => 'onMessage',
];
}
}

View File

@@ -1,25 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\EventListener;
/**
* Encrypts messages using S/MIME.
*
* @author Florent Morselli <florent.morselli@spomky-labs.com>
*/
interface SmimeCertificateRepositoryInterface
{
/**
* @return ?string The path to the certificate. null if not found
*/
public function findCertificatePathFor(string $email): ?string;
}

View File

@@ -1,64 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Mime\Crypto\SMimeEncrypter;
use Symfony\Component\Mime\Message;
/**
* Encrypts messages using S/MIME.
*
* @author Elías Fernández
*/
final class SmimeEncryptedMessageListener implements EventSubscriberInterface
{
public function __construct(
private readonly SmimeCertificateRepositoryInterface $smimeRepository,
private readonly ?int $cipher = null,
) {
}
public function onMessage(MessageEvent $event): void
{
$message = $event->getMessage();
if (!$message instanceof Message) {
return;
}
if (!$message->getHeaders()->has('X-SMime-Encrypt')) {
return;
}
$message->getHeaders()->remove('X-SMime-Encrypt');
$certificatePaths = [];
foreach ($event->getEnvelope()->getRecipients() as $recipient) {
$certificatePath = $this->smimeRepository->findCertificatePathFor($recipient->getAddress());
if (null === $certificatePath) {
return;
}
$certificatePaths[] = $certificatePath;
}
if (0 === \count($certificatePaths)) {
return;
}
$encrypter = new SMimeEncrypter($certificatePaths, $this->cipher);
$event->setMessage($encrypter->encrypt($message));
}
public static function getSubscribedEvents(): array
{
return [
MessageEvent::class => ['onMessage', -128],
];
}
}

View File

@@ -1,47 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Mime\Crypto\SMimeSigner;
use Symfony\Component\Mime\Message;
/**
* Signs messages using S/MIME.
*
* @author Elías Fernández
*/
class SmimeSignedMessageListener implements EventSubscriberInterface
{
public function __construct(
private SMimeSigner $signer,
) {
}
public function onMessage(MessageEvent $event): void
{
$message = $event->getMessage();
if (!$message instanceof Message) {
return;
}
$event->setMessage($this->signer->sign($message));
}
public static function getSubscribedEvents(): array
{
return [
MessageEvent::class => ['onMessage', -128],
];
}
}

View File

@@ -1,21 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Exception;
/**
* Exception interface for all exceptions thrown by the component.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface ExceptionInterface extends \Throwable
{
}

View File

@@ -1,34 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Exception;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class HttpTransportException extends TransportException
{
public function __construct(
string $message,
private ResponseInterface $response,
int $code = 0,
?\Throwable $previous = null,
) {
parent::__construct($message, $code, $previous);
}
public function getResponse(): ResponseInterface
{
return $this->response;
}
}

View File

@@ -1,19 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Exception;
/**
* @author Konstantin Myakshin <molodchick@gmail.com>
*/
class IncompleteDsnException extends InvalidArgumentException
{
}

View File

@@ -1,19 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Exception;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}

View File

@@ -1,19 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Exception;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class LogicException extends \LogicException implements ExceptionInterface
{
}

View File

@@ -1,19 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Exception;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}

View File

@@ -1,30 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Exception;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class TransportException extends RuntimeException implements TransportExceptionInterface
{
private string $debug = '';
public function getDebug(): string
{
return $this->debug;
}
public function appendDebug(string $debug): void
{
$this->debug .= $debug;
}
}

View File

@@ -1,22 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Exception;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
interface TransportExceptionInterface extends ExceptionInterface
{
public function getDebug(): string;
public function appendDebug(string $debug): void;
}

View File

@@ -1,16 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Exception;
class UnexpectedResponseException extends TransportException
{
}

View File

@@ -1,121 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Exception;
use Symfony\Component\Mailer\Bridge;
use Symfony\Component\Mailer\Transport\Dsn;
/**
* @author Konstantin Myakshin <molodchick@gmail.com>
*/
class UnsupportedSchemeException extends LogicException
{
private const SCHEME_TO_PACKAGE_MAP = [
'ahasend' => [
'class' => Bridge\AhaSend\Transport\AhaSendTransportFactory::class,
'package' => 'symfony/aha-send-mailer',
],
'azure' => [
'class' => Bridge\Azure\Transport\AzureTransportFactory::class,
'package' => 'symfony/azure-mailer',
],
'brevo' => [
'class' => Bridge\Brevo\Transport\BrevoTransportFactory::class,
'package' => 'symfony/brevo-mailer',
],
'gmail' => [
'class' => Bridge\Google\Transport\GmailTransportFactory::class,
'package' => 'symfony/google-mailer',
],
'infobip' => [
'class' => Bridge\Infobip\Transport\InfobipTransportFactory::class,
'package' => 'symfony/infobip-mailer',
],
'mailersend' => [
'class' => Bridge\MailerSend\Transport\MailerSendTransportFactory::class,
'package' => 'symfony/mailersend-mailer',
],
'mailgun' => [
'class' => Bridge\Mailgun\Transport\MailgunTransportFactory::class,
'package' => 'symfony/mailgun-mailer',
],
'mailjet' => [
'class' => Bridge\Mailjet\Transport\MailjetTransportFactory::class,
'package' => 'symfony/mailjet-mailer',
],
'mailomat' => [
'class' => Bridge\Mailomat\Transport\MailomatTransportFactory::class,
'package' => 'symfony/mailomat-mailer',
],
'mailpace' => [
'class' => Bridge\MailPace\Transport\MailPaceTransportFactory::class,
'package' => 'symfony/mail-pace-mailer',
],
'mandrill' => [
'class' => Bridge\Mailchimp\Transport\MandrillTransportFactory::class,
'package' => 'symfony/mailchimp-mailer',
],
'postal' => [
'class' => Bridge\Postal\Transport\PostalTransportFactory::class,
'package' => 'symfony/postal-mailer',
],
'postmark' => [
'class' => Bridge\Postmark\Transport\PostmarkTransportFactory::class,
'package' => 'symfony/postmark-mailer',
],
'mailtrap' => [
'class' => Bridge\Mailtrap\Transport\MailtrapTransportFactory::class,
'package' => 'symfony/mailtrap-mailer',
],
'resend' => [
'class' => Bridge\Resend\Transport\ResendTransportFactory::class,
'package' => 'symfony/resend-mailer',
],
'scaleway' => [
'class' => Bridge\Scaleway\Transport\ScalewayTransportFactory::class,
'package' => 'symfony/scaleway-mailer',
],
'sendgrid' => [
'class' => Bridge\Sendgrid\Transport\SendgridTransportFactory::class,
'package' => 'symfony/sendgrid-mailer',
],
'ses' => [
'class' => Bridge\Amazon\Transport\SesTransportFactory::class,
'package' => 'symfony/amazon-mailer',
],
'sweego' => [
'class' => Bridge\Sweego\Transport\SweegoTransportFactory::class,
'package' => 'symfony/sweego-mailer',
],
];
public function __construct(Dsn $dsn, ?string $name = null, array $supported = [])
{
$provider = $dsn->getScheme();
if (false !== $pos = strpos($provider, '+')) {
$provider = substr($provider, 0, $pos);
}
$package = self::SCHEME_TO_PACKAGE_MAP[$provider] ?? null;
if ($package && !class_exists($package['class'])) {
parent::__construct(\sprintf('Unable to send emails via "%s" as the bridge is not installed. Try running "composer require %s".', $provider, $package['package']));
return;
}
$message = \sprintf('The "%s" scheme is not supported', $dsn->getScheme());
if ($name && $supported) {
$message .= \sprintf('; supported schemes for mailer "%s" are: "%s"', $name, implode('", "', $supported));
}
parent::__construct($message.'.');
}
}

View File

@@ -1,32 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Header;
use Symfony\Component\Mime\Header\UnstructuredHeader;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
final class MetadataHeader extends UnstructuredHeader
{
public function __construct(
private string $key,
string $value,
) {
parent::__construct('X-Metadata-'.$key, $value);
}
public function getKey(): string
{
return $this->key;
}
}

View File

@@ -1,25 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Header;
use Symfony\Component\Mime\Header\UnstructuredHeader;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
final class TagHeader extends UnstructuredHeader
{
public function __construct(string $value)
{
parent::__construct('X-Tag', $value);
}
}

View File

@@ -1,19 +0,0 @@
Copyright (c) 2019-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -1,72 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Mailer\Event\MessageEvent;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\Messenger\SendEmailMessage;
use Symfony\Component\Mailer\Transport\TransportInterface;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Mime\RawMessage;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class Mailer implements MailerInterface
{
public function __construct(
private TransportInterface $transport,
private ?MessageBusInterface $bus = null,
private ?EventDispatcherInterface $dispatcher = null,
) {
}
public function send(RawMessage $message, ?Envelope $envelope = null): void
{
if (null === $this->bus) {
$this->transport->send($message, $envelope);
return;
}
$stamps = [];
if (null !== $this->dispatcher) {
// The dispatched event here has `queued` set to `true`; the goal is NOT to render the message, but to let
// listeners do something before a message is sent to the queue.
// We are using a cloned message as we still want to dispatch the **original** message, not the one modified by listeners.
// That's because the listeners will run again when the email is sent via Messenger by the transport (see `AbstractTransport`).
// Listeners should act depending on the `$queued` argument of the `MessageEvent` instance.
$clonedMessage = clone $message;
$clonedEnvelope = null !== $envelope ? clone $envelope : Envelope::create($clonedMessage);
$event = new MessageEvent($clonedMessage, $clonedEnvelope, (string) $this->transport, true);
$this->dispatcher->dispatch($event);
$stamps = $event->getStamps();
if ($event->isRejected()) {
return;
}
}
try {
$this->bus->dispatch(new SendEmailMessage($message, $envelope), $stamps);
} catch (HandlerFailedException $e) {
foreach ($e->getWrappedExceptions() as $nested) {
if ($nested instanceof TransportExceptionInterface) {
throw $nested;
}
}
throw $e;
}
}
}

View File

@@ -1,30 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mime\RawMessage;
/**
* Interface for mailers able to send emails synchronously and/or asynchronously.
*
* Implementations must support synchronous and asynchronous sending.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface MailerInterface
{
/**
* @throws TransportExceptionInterface
*/
public function send(RawMessage $message, ?Envelope $envelope = null): void;
}

View File

@@ -1,31 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Messenger;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mailer\Transport\TransportInterface;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class MessageHandler
{
public function __construct(
private TransportInterface $transport,
) {
}
public function __invoke(SendEmailMessage $message): ?SentMessage
{
return $this->transport->send($message->getMessage(), $message->getEnvelope());
}
}

View File

@@ -1,37 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Messenger;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mime\RawMessage;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class SendEmailMessage
{
public function __construct(
private RawMessage $message,
private ?Envelope $envelope = null,
) {
}
public function getMessage(): RawMessage
{
return $this->message;
}
public function getEnvelope(): ?Envelope
{
return $this->envelope;
}
}

View File

@@ -1,87 +0,0 @@
Mailer Component
================
The Mailer component helps sending emails.
Getting Started
---------------
```bash
composer require symfony/mailer
```
```php
use Symfony\Component\Mailer\Transport;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mime\Email;
$transport = Transport::fromDsn('smtp://localhost');
$mailer = new Mailer($transport);
$email = (new Email())
->from('hello@example.com')
->to('you@example.com')
//->cc('cc@example.com')
//->bcc('bcc@example.com')
//->replyTo('fabien@example.com')
//->priority(Email::PRIORITY_HIGH)
->subject('Time for Symfony Mailer!')
->text('Sending emails is fun again!')
->html('<p>See Twig integration for better HTML integration!</p>');
$mailer->send($email);
```
To enable the Twig integration of the Mailer, require `symfony/twig-bridge` and
set up the `BodyRenderer`:
```php
use Symfony\Bridge\Twig\Mime\BodyRenderer;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Mailer\EventListener\MessageListener;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\Transport;
use Twig\Environment as TwigEnvironment;
$twig = new TwigEnvironment(...);
$messageListener = new MessageListener(null, new BodyRenderer($twig));
$eventDispatcher = new EventDispatcher();
$eventDispatcher->addSubscriber($messageListener);
$transport = Transport::fromDsn('smtp://localhost', $eventDispatcher);
$mailer = new Mailer($transport, null, $eventDispatcher);
$email = (new TemplatedEmail())
// ...
->htmlTemplate('emails/signup.html.twig')
->context([
'expiration_date' => new \DateTimeImmutable('+7 days'),
'username' => 'foo',
])
;
$mailer->send($email);
```
Sponsor
-------
The Mailer component for Symfony 7.2 is [backed][1] by:
* [Sweego][2], a European email and SMS sending platform for developers and product builders. Easily create, deliver, and monitor your emails and notifications.
Help Symfony by [sponsoring][3] its development!
Resources
---------
* [Documentation](https://symfony.com/doc/current/mailer.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)
[1]: https://symfony.com/backers
[2]: https://www.sweego.io/
[3]: https://symfony.com/sponsor

View File

@@ -1,95 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer;
use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\RawMessage;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class SentMessage
{
private RawMessage $original;
private RawMessage $raw;
private string $messageId;
private string $debug = '';
/**
* @internal
*/
public function __construct(
RawMessage $message,
private Envelope $envelope,
) {
$message->ensureValidity();
$this->original = $message;
if ($message instanceof Message) {
$message = clone $message;
$headers = $message->getHeaders();
if (!$headers->has('Message-ID')) {
$headers->addIdHeader('Message-ID', $message->generateMessageId());
}
$this->messageId = $headers->get('Message-ID')->getId();
$this->raw = new RawMessage($message->toIterable());
} else {
$this->raw = $message;
}
}
public function getMessage(): RawMessage
{
return $this->raw;
}
public function getOriginalMessage(): RawMessage
{
return $this->original;
}
public function getEnvelope(): Envelope
{
return $this->envelope;
}
public function setMessageId(string $id): void
{
$this->messageId = $id;
}
public function getMessageId(): string
{
return $this->messageId;
}
public function getDebug(): string
{
return $this->debug;
}
public function appendDebug(string $debug): void
{
$this->debug .= $debug;
}
public function toString(): string
{
return $this->raw->toString();
}
public function toIterable(): iterable
{
return $this->raw->toIterable();
}
}

View File

@@ -1,83 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Test;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
use Symfony\Component\Mailer\Transport\Dsn;
use Symfony\Component\Mailer\Transport\TransportFactoryInterface;
use Symfony\Component\Mailer\Transport\TransportInterface;
abstract class AbstractTransportFactoryTestCase extends TestCase
{
protected const USER = 'u$er';
protected const PASSWORD = 'pa$s';
abstract public function getFactory(): TransportFactoryInterface;
/**
* @psalm-return iterable<array{0: Dsn, 1: bool}>
*/
abstract public static function supportsProvider(): iterable;
/**
* @psalm-return iterable<array{0: Dsn, 1: TransportInterface}>
*/
abstract public static function createProvider(): iterable;
/**
* @psalm-return iterable<array{0: Dsn, 1?: string|null}>
*/
abstract public static function unsupportedSchemeProvider(): iterable;
/**
* @dataProvider supportsProvider
*/
#[DataProvider('supportsProvider')]
public function testSupports(Dsn $dsn, bool $supports)
{
$factory = $this->getFactory();
$this->assertSame($supports, $factory->supports($dsn));
}
/**
* @dataProvider createProvider
*/
#[DataProvider('createProvider')]
public function testCreate(Dsn $dsn, TransportInterface $transport)
{
$factory = $this->getFactory();
$this->assertEquals($transport, $factory->create($dsn));
if (str_contains('smtp', $dsn->getScheme())) {
$this->assertStringMatchesFormat($dsn->getScheme().'://%S'.$dsn->getHost().'%S', (string) $transport);
}
}
/**
* @dataProvider unsupportedSchemeProvider
*/
#[DataProvider('unsupportedSchemeProvider')]
public function testUnsupportedSchemeException(Dsn $dsn, ?string $message = null)
{
$factory = $this->getFactory();
$this->expectException(UnsupportedSchemeException::class);
if (null !== $message) {
$this->expectExceptionMessage($message);
}
$factory->create($dsn);
}
}

View File

@@ -1,61 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Test\Constraint;
use PHPUnit\Framework\Constraint\Constraint;
use Symfony\Component\Mailer\Event\MessageEvents;
final class EmailCount extends Constraint
{
public function __construct(
private int $expectedValue,
private ?string $transport = null,
private bool $queued = false,
) {
}
public function toString(): string
{
return \sprintf('%shas %s "%d" emails', $this->transport ? $this->transport.' ' : '', $this->queued ? 'queued' : 'sent', $this->expectedValue);
}
/**
* @param MessageEvents $events
*/
protected function matches($events): bool
{
return $this->expectedValue === $this->countEmails($events);
}
/**
* @param MessageEvents $events
*/
protected function failureDescription($events): string
{
return \sprintf('the Transport %s (%d %s)', $this->toString(), $this->countEmails($events), $this->queued ? 'queued' : 'sent');
}
private function countEmails(MessageEvents $events): int
{
$count = 0;
foreach ($events->getEvents($this->transport) as $event) {
if (
($this->queued && $event->isQueued())
|| (!$this->queued && !$event->isQueued())
) {
++$count;
}
}
return $count;
}
}

View File

@@ -1,39 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Test\Constraint;
use PHPUnit\Framework\Constraint\Constraint;
use Symfony\Component\Mailer\Event\MessageEvent;
final class EmailIsQueued extends Constraint
{
public function toString(): string
{
return 'is queued';
}
/**
* @param MessageEvent $event
*/
protected function matches($event): bool
{
return $event->isQueued();
}
/**
* @param MessageEvent $event
*/
protected function failureDescription($event): string
{
return 'the Email '.$this->toString();
}
}

View File

@@ -1,36 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Test;
use PHPUnit\Framework\Attributes\DataProvider;
use Symfony\Component\Mailer\Exception\IncompleteDsnException;
use Symfony\Component\Mailer\Transport\Dsn;
trait IncompleteDsnTestTrait
{
/**
* @psalm-return iterable<array{0: Dsn}>
*/
abstract public static function incompleteDsnProvider(): iterable;
/**
* @dataProvider incompleteDsnProvider
*/
#[DataProvider('incompleteDsnProvider')]
public function testIncompleteDsnException(Dsn $dsn)
{
$factory = $this->getFactory();
$this->expectException(IncompleteDsnException::class);
$factory->create($dsn);
}
}

View File

@@ -1,64 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Test;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Transport\Dsn;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* A test case to ease testing Transport Factory.
*
* @author Konstantin Myakshin <molodchick@gmail.com>
*
* @deprecated since Symfony 7.2, use AbstractTransportFactoryTestCase instead
*/
abstract class TransportFactoryTestCase extends AbstractTransportFactoryTestCase
{
use IncompleteDsnTestTrait;
protected EventDispatcherInterface $dispatcher;
protected HttpClientInterface $client;
protected LoggerInterface $logger;
/**
* @psalm-return iterable<array{0: Dsn, 1?: string|null}>
*/
public static function unsupportedSchemeProvider(): iterable
{
return [];
}
/**
* @psalm-return iterable<array{0: Dsn}>
*/
public static function incompleteDsnProvider(): iterable
{
return [];
}
protected function getDispatcher(): EventDispatcherInterface
{
return $this->dispatcher ??= $this->createMock(EventDispatcherInterface::class);
}
protected function getClient(): HttpClientInterface
{
return $this->client ??= $this->createMock(HttpClientInterface::class);
}
protected function getLogger(): LoggerInterface
{
return $this->logger ??= $this->createMock(LoggerInterface::class);
}
}

View File

@@ -1,201 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Bridge\AhaSend\Transport\AhaSendTransportFactory;
use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesTransportFactory;
use Symfony\Component\Mailer\Bridge\Azure\Transport\AzureTransportFactory;
use Symfony\Component\Mailer\Bridge\Brevo\Transport\BrevoTransportFactory;
use Symfony\Component\Mailer\Bridge\Google\Transport\GmailTransportFactory;
use Symfony\Component\Mailer\Bridge\Infobip\Transport\InfobipTransportFactory;
use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillTransportFactory;
use Symfony\Component\Mailer\Bridge\MailerSend\Transport\MailerSendTransportFactory;
use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory;
use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory;
use Symfony\Component\Mailer\Bridge\Mailomat\Transport\MailomatTransportFactory;
use Symfony\Component\Mailer\Bridge\MailPace\Transport\MailPaceTransportFactory;
use Symfony\Component\Mailer\Bridge\Mailtrap\Transport\MailtrapTransportFactory;
use Symfony\Component\Mailer\Bridge\Postal\Transport\PostalTransportFactory;
use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory;
use Symfony\Component\Mailer\Bridge\Resend\Transport\ResendTransportFactory;
use Symfony\Component\Mailer\Bridge\Scaleway\Transport\ScalewayTransportFactory;
use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridTransportFactory;
use Symfony\Component\Mailer\Bridge\Sweego\Transport\SweegoTransportFactory;
use Symfony\Component\Mailer\Exception\InvalidArgumentException;
use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
use Symfony\Component\Mailer\Transport\Dsn;
use Symfony\Component\Mailer\Transport\FailoverTransport;
use Symfony\Component\Mailer\Transport\NativeTransportFactory;
use Symfony\Component\Mailer\Transport\NullTransportFactory;
use Symfony\Component\Mailer\Transport\RoundRobinTransport;
use Symfony\Component\Mailer\Transport\SendmailTransportFactory;
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransportFactory;
use Symfony\Component\Mailer\Transport\TransportFactoryInterface;
use Symfony\Component\Mailer\Transport\TransportInterface;
use Symfony\Component\Mailer\Transport\Transports;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Konstantin Myakshin <molodchick@gmail.com>
*/
final class Transport
{
private const FACTORY_CLASSES = [
AhaSendTransportFactory::class,
AzureTransportFactory::class,
BrevoTransportFactory::class,
GmailTransportFactory::class,
InfobipTransportFactory::class,
MailerSendTransportFactory::class,
MailgunTransportFactory::class,
MailjetTransportFactory::class,
MailomatTransportFactory::class,
MailPaceTransportFactory::class,
MandrillTransportFactory::class,
PostalTransportFactory::class,
PostmarkTransportFactory::class,
MailtrapTransportFactory::class,
ResendTransportFactory::class,
ScalewayTransportFactory::class,
SendgridTransportFactory::class,
SesTransportFactory::class,
SweegoTransportFactory::class,
];
public static function fromDsn(#[\SensitiveParameter] string $dsn, ?EventDispatcherInterface $dispatcher = null, ?HttpClientInterface $client = null, ?LoggerInterface $logger = null): TransportInterface
{
$factory = new self(iterator_to_array(self::getDefaultFactories($dispatcher, $client, $logger)));
return $factory->fromString($dsn);
}
public static function fromDsns(#[\SensitiveParameter] array $dsns, ?EventDispatcherInterface $dispatcher = null, ?HttpClientInterface $client = null, ?LoggerInterface $logger = null): TransportInterface
{
$factory = new self(iterator_to_array(self::getDefaultFactories($dispatcher, $client, $logger)));
return $factory->fromStrings($dsns);
}
/**
* @param TransportFactoryInterface[] $factories
*/
public function __construct(
private iterable $factories,
) {
}
public function fromStrings(#[\SensitiveParameter] array $dsns): Transports
{
$transports = [];
foreach ($dsns as $name => $dsn) {
$transports[$name] = $this->fromString($dsn);
}
return new Transports($transports);
}
public function fromString(#[\SensitiveParameter] string $dsn): TransportInterface
{
[$transport, $offset] = $this->parseDsn($dsn);
if ($offset !== \strlen($dsn)) {
throw new InvalidArgumentException('The mailer DSN has some garbage at the end.');
}
return $transport;
}
private function parseDsn(#[\SensitiveParameter] string $dsn, int $offset = 0): array
{
static $keywords = [
'failover' => FailoverTransport::class,
'roundrobin' => RoundRobinTransport::class,
];
while (true) {
foreach ($keywords as $name => $class) {
$name .= '(';
if ($name === substr($dsn, $offset, \strlen($name))) {
$offset += \strlen($name) - 1;
preg_match('{\(([^()]|(?R))*\)}A', $dsn, $matches, 0, $offset);
if (!isset($matches[0])) {
continue;
}
++$offset;
$args = [];
while (true) {
[$arg, $offset] = $this->parseDsn($dsn, $offset);
$args[] = $arg;
if (\strlen($dsn) === $offset) {
break;
}
++$offset;
if (')' === $dsn[$offset - 1]) {
break;
}
}
parse_str(substr($dsn, $offset + 1), $query);
if ($period = $query['retry_period'] ?? 0) {
return [new $class($args, (int) $period), $offset + \strlen('retry_period='.$period) + 1];
}
return [new $class($args), $offset];
}
}
if (preg_match('{(\w+)\(}A', $dsn, $matches, 0, $offset)) {
throw new InvalidArgumentException(\sprintf('The "%s" keyword is not valid (valid ones are "%s"), ', $matches[1], implode('", "', array_keys($keywords))));
}
if ($pos = strcspn($dsn, ' )', $offset)) {
return [$this->fromDsnObject(Dsn::fromString(substr($dsn, $offset, $pos))), $offset + $pos];
}
return [$this->fromDsnObject(Dsn::fromString(substr($dsn, $offset))), \strlen($dsn)];
}
}
public function fromDsnObject(Dsn $dsn): TransportInterface
{
foreach ($this->factories as $factory) {
if ($factory->supports($dsn)) {
return $factory->create($dsn);
}
}
throw new UnsupportedSchemeException($dsn);
}
/**
* @return \Traversable<int, TransportFactoryInterface>
*/
public static function getDefaultFactories(?EventDispatcherInterface $dispatcher = null, ?HttpClientInterface $client = null, ?LoggerInterface $logger = null): \Traversable
{
foreach (self::FACTORY_CLASSES as $factoryClass) {
if (class_exists($factoryClass)) {
yield new $factoryClass($dispatcher, $client, $logger);
}
}
yield new NullTransportFactory($dispatcher, $client, $logger);
yield new SendmailTransportFactory($dispatcher, $client, $logger);
yield new EsmtpTransportFactory($dispatcher, $client, $logger);
yield new NativeTransportFactory($dispatcher, $client, $logger);
}
}

View File

@@ -1,47 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Transport;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Exception\RuntimeException;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
use Symfony\Component\Mime\MessageConverter;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
abstract class AbstractApiTransport extends AbstractHttpTransport
{
abstract protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $envelope): ResponseInterface;
protected function doSendHttp(SentMessage $message): ResponseInterface
{
try {
$email = MessageConverter::toEmail($message->getOriginalMessage());
} catch (\Exception $e) {
throw new RuntimeException(\sprintf('Unable to send message with the "%s" transport: ', __CLASS__).$e->getMessage(), 0, $e);
}
return $this->doSendApi($message, $email, $message->getEnvelope());
}
/**
* @return Address[]
*/
protected function getRecipients(Email $email, Envelope $envelope): array
{
return array_filter($envelope->getRecipients(), fn (Address $address) => false === \in_array($address, array_merge($email->getCc(), $email->getBcc()), true));
}
}

View File

@@ -1,79 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Transport;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\Mailer\Exception\HttpTransportException;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* @author Victor Bocharsky <victor@symfonycasts.com>
*/
abstract class AbstractHttpTransport extends AbstractTransport
{
protected ?string $host = null;
protected ?int $port = null;
public function __construct(
protected ?HttpClientInterface $client = null,
?EventDispatcherInterface $dispatcher = null,
?LoggerInterface $logger = null,
) {
if (null === $client) {
if (!class_exists(HttpClient::class)) {
throw new \LogicException(\sprintf('You cannot use "%s" as the HttpClient component is not installed. Try running "composer require symfony/http-client".', __CLASS__));
}
$this->client = HttpClient::create();
}
parent::__construct($dispatcher, $logger);
}
/**
* @return $this
*/
public function setHost(?string $host): static
{
$this->host = $host;
return $this;
}
/**
* @return $this
*/
public function setPort(?int $port): static
{
$this->port = $port;
return $this;
}
abstract protected function doSendHttp(SentMessage $message): ResponseInterface;
protected function doSend(SentMessage $message): void
{
try {
$response = $this->doSendHttp($message);
$message->appendDebug($response->getInfo('debug') ?? '');
} catch (HttpTransportException $e) {
$e->appendDebug($e->getResponse()->getInfo('debug') ?? '');
throw $e;
}
}
}

View File

@@ -1,136 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Transport;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Event\FailedMessageEvent;
use Symfony\Component\Mailer\Event\MessageEvent;
use Symfony\Component\Mailer\Event\SentMessageEvent;
use Symfony\Component\Mailer\Exception\LogicException;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\BodyRendererInterface;
use Symfony\Component\Mime\RawMessage;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
abstract class AbstractTransport implements TransportInterface
{
private LoggerInterface $logger;
private float $rate = 0;
private float $lastSent = 0;
public function __construct(
private ?EventDispatcherInterface $dispatcher = null,
?LoggerInterface $logger = null,
) {
$this->logger = $logger ?? new NullLogger();
}
/**
* Sets the maximum number of messages to send per second (0 to disable).
*
* @return $this
*/
public function setMaxPerSecond(float $rate): static
{
if (0 >= $rate) {
$rate = 0;
}
$this->rate = $rate;
$this->lastSent = 0;
return $this;
}
public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage
{
$message = clone $message;
$envelope = null !== $envelope ? clone $envelope : Envelope::create($message);
try {
if (!$this->dispatcher) {
$sentMessage = new SentMessage($message, $envelope);
$this->doSend($sentMessage);
return $sentMessage;
}
$event = new MessageEvent($message, $envelope, (string) $this);
$this->dispatcher->dispatch($event);
if ($event->isRejected()) {
return null;
}
$envelope = $event->getEnvelope();
$message = $event->getMessage();
if ($message instanceof TemplatedEmail && !$message->isRendered()) {
throw new LogicException(\sprintf('You must configure a "%s" when a "%s" instance has a text or HTML template set.', BodyRendererInterface::class, get_debug_type($message)));
}
$sentMessage = new SentMessage($message, $envelope);
try {
$this->doSend($sentMessage);
} catch (\Throwable $error) {
$this->dispatcher->dispatch(new FailedMessageEvent($message, $error));
$this->checkThrottling();
throw $error;
}
$this->dispatcher->dispatch(new SentMessageEvent($sentMessage));
return $sentMessage;
} finally {
$this->checkThrottling();
}
}
abstract protected function doSend(SentMessage $message): void;
/**
* @param Address[] $addresses
*
* @return string[]
*/
protected function stringifyAddresses(array $addresses): array
{
return array_map(fn (Address $a) => $a->toString(), $addresses);
}
protected function getLogger(): LoggerInterface
{
return $this->logger;
}
private function checkThrottling(): void
{
if (0 == $this->rate) {
return;
}
$sleep = (1 / $this->rate) - (microtime(true) - $this->lastSent);
if (0 < $sleep) {
$this->logger->debug(\sprintf('Email transport "%s" sleeps for %.2f seconds', __CLASS__, $sleep));
usleep((int) ($sleep * 1000000));
}
$this->lastSent = microtime(true);
}
}

View File

@@ -1,47 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Transport;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Exception\IncompleteDsnException;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* @author Konstantin Myakshin <molodchick@gmail.com>
*/
abstract class AbstractTransportFactory implements TransportFactoryInterface
{
public function __construct(
protected ?EventDispatcherInterface $dispatcher = null,
protected ?HttpClientInterface $client = null,
protected ?LoggerInterface $logger = null,
) {
}
public function supports(Dsn $dsn): bool
{
return \in_array($dsn->getScheme(), $this->getSupportedSchemes(), true);
}
abstract protected function getSupportedSchemes(): array;
protected function getUser(Dsn $dsn): string
{
return $dsn->getUser() ?? throw new IncompleteDsnException('User is not set.');
}
protected function getPassword(Dsn $dsn): string
{
return $dsn->getPassword() ?? throw new IncompleteDsnException('Password is not set.');
}
}

View File

@@ -1,87 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Transport;
use Symfony\Component\Mailer\Exception\InvalidArgumentException;
/**
* @author Konstantin Myakshin <molodchick@gmail.com>
*/
final class Dsn
{
public function __construct(
private string $scheme,
private string $host,
private ?string $user = null,
#[\SensitiveParameter] private ?string $password = null,
private ?int $port = null,
private array $options = [],
) {
}
public static function fromString(#[\SensitiveParameter] string $dsn): self
{
if (false === $params = parse_url($dsn)) {
throw new InvalidArgumentException('The mailer DSN is invalid.');
}
if (!isset($params['scheme'])) {
throw new InvalidArgumentException('The mailer DSN must contain a scheme.');
}
if (!isset($params['host'])) {
throw new InvalidArgumentException('The mailer DSN must contain a host (use "default" by default).');
}
$user = '' !== ($params['user'] ?? '') ? rawurldecode($params['user']) : null;
$password = '' !== ($params['pass'] ?? '') ? rawurldecode($params['pass']) : null;
$port = $params['port'] ?? null;
parse_str($params['query'] ?? '', $query);
return new self($params['scheme'], $params['host'], $user, $password, $port, $query);
}
public function getScheme(): string
{
return $this->scheme;
}
public function getHost(): string
{
return $this->host;
}
public function getUser(): ?string
{
return $this->user;
}
public function getPassword(): ?string
{
return $this->password;
}
public function getPort(?int $default = null): ?int
{
return $this->port ?? $default;
}
public function getOption(string $key, mixed $default = null): mixed
{
return $this->options[$key] ?? $default;
}
public function getBooleanOption(string $key, bool $default = false): bool
{
return filter_var($this->getOption($key, $default), \FILTER_VALIDATE_BOOLEAN);
}
}

View File

@@ -1,41 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Transport;
/**
* Uses several Transports using a failover algorithm.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class FailoverTransport extends RoundRobinTransport
{
private ?TransportInterface $currentTransport = null;
protected function getNextTransport(): ?TransportInterface
{
if (null === $this->currentTransport || $this->isTransportDead($this->currentTransport)) {
$this->currentTransport = parent::getNextTransport();
}
return $this->currentTransport;
}
protected function getInitialCursor(): int
{
return 0;
}
protected function getNameSymbol(): string
{
return 'failover';
}
}

View File

@@ -1,63 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Transport;
use Symfony\Component\Mailer\Exception\TransportException;
use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
use Symfony\Component\Mailer\Transport\Smtp\SmtpTransport;
use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream;
/**
* Factory that configures a transport (sendmail or SMTP) based on php.ini settings.
*
* @author Laurent VOULLEMIER <laurent.voullemier@gmail.com>
*/
final class NativeTransportFactory extends AbstractTransportFactory
{
public function create(Dsn $dsn): TransportInterface
{
if (!\in_array($dsn->getScheme(), $this->getSupportedSchemes(), true)) {
throw new UnsupportedSchemeException($dsn, 'native', $this->getSupportedSchemes());
}
if ($sendMailPath = ini_get('sendmail_path')) {
return new SendmailTransport($sendMailPath, $this->dispatcher, $this->logger);
}
if ('\\' !== \DIRECTORY_SEPARATOR) {
throw new TransportException('sendmail_path is not configured in php.ini.');
}
// Only for windows hosts; at this point non-windows
// host have already thrown an exception or returned a transport
$host = ini_get('SMTP');
$port = (int) ini_get('smtp_port');
if (!$host || !$port) {
throw new TransportException('smtp or smtp_port is not configured in php.ini.');
}
$socketStream = new SocketStream();
$socketStream->setHost($host);
$socketStream->setPort($port);
if (465 !== $port) {
$socketStream->disableTls();
}
return new SmtpTransport($socketStream, $this->dispatcher, $this->logger);
}
protected function getSupportedSchemes(): array
{
return ['native'];
}
}

View File

@@ -1,31 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Transport;
use Symfony\Component\Mailer\SentMessage;
/**
* Pretends messages have been sent, but just ignores them.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class NullTransport extends AbstractTransport
{
protected function doSend(SentMessage $message): void
{
}
public function __toString(): string
{
return 'null://';
}
}

View File

@@ -1,34 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Transport;
use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
/**
* @author Konstantin Myakshin <molodchick@gmail.com>
*/
final class NullTransportFactory extends AbstractTransportFactory
{
public function create(Dsn $dsn): TransportInterface
{
if ('null' === $dsn->getScheme()) {
return new NullTransport($this->dispatcher, $this->logger);
}
throw new UnsupportedSchemeException($dsn, 'null', $this->getSupportedSchemes());
}
protected function getSupportedSchemes(): array
{
return ['null'];
}
}

View File

@@ -1,123 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Transport;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Exception\TransportException;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mime\RawMessage;
/**
* Uses several Transports using a round robin algorithm.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class RoundRobinTransport implements TransportInterface
{
/**
* @var \SplObjectStorage<TransportInterface, float>
*/
private \SplObjectStorage $deadTransports;
private int $cursor = -1;
/**
* @param TransportInterface[] $transports
*/
public function __construct(
private array $transports,
private int $retryPeriod = 60,
) {
if (!$transports) {
throw new TransportException(\sprintf('"%s" must have at least one transport configured.', static::class));
}
$this->deadTransports = new \SplObjectStorage();
}
public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage
{
$exception = null;
while ($transport = $this->getNextTransport()) {
try {
return $transport->send(clone $message, $envelope);
} catch (TransportExceptionInterface $e) {
$exception ??= new TransportException('All transports failed.');
$exception->appendDebug(\sprintf("Transport \"%s\": %s\n", $transport, $e->getDebug()));
$this->deadTransports[$transport] = microtime(true);
}
}
throw $exception ?? new TransportException('No transports found.');
}
public function __toString(): string
{
return $this->getNameSymbol().'('.implode(' ', array_map('strval', $this->transports)).')';
}
/**
* Rotates the transport list around and returns the first instance.
*/
protected function getNextTransport(): ?TransportInterface
{
if (-1 === $this->cursor) {
$this->cursor = $this->getInitialCursor();
}
$cursor = $this->cursor;
while (true) {
$transport = $this->transports[$cursor];
if (!$this->isTransportDead($transport)) {
break;
}
if ((microtime(true) - $this->deadTransports[$transport]) > $this->retryPeriod) {
unset($this->deadTransports[$transport]);
break;
}
if ($this->cursor === $cursor = $this->moveCursor($cursor)) {
return null;
}
}
$this->cursor = $this->moveCursor($cursor);
return $transport;
}
protected function isTransportDead(TransportInterface $transport): bool
{
return $this->deadTransports->offsetExists($transport);
}
protected function getInitialCursor(): int
{
// the cursor initial value is randomized so that
// when are not in a daemon, we are still rotating the transports
return mt_rand(0, \count($this->transports) - 1);
}
protected function getNameSymbol(): string
{
return 'roundrobin';
}
private function moveCursor(int $cursor): int
{
return ++$cursor >= \count($this->transports) ? 0 : $cursor;
}
}

View File

@@ -1,124 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Transport;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mailer\Transport\Smtp\SmtpTransport;
use Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream;
use Symfony\Component\Mailer\Transport\Smtp\Stream\ProcessStream;
use Symfony\Component\Mime\RawMessage;
/**
* SendmailTransport for sending mail through a Sendmail/Postfix (etc..) binary.
*
* Transport can be instantiated through SendmailTransportFactory or NativeTransportFactory:
*
* - SendmailTransportFactory to use most common sendmail path and recommended options
* - NativeTransportFactory when configuration is set via php.ini
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Chris Corbyn
*/
class SendmailTransport extends AbstractTransport
{
private string $command = '/usr/sbin/sendmail -bs';
private ProcessStream $stream;
private ?SmtpTransport $transport = null;
/**
* Constructor.
*
* Supported modes are -bs and -t, with any additional flags desired.
*
* The recommended mode is "-bs" since it is interactive and failure notifications are hence possible.
* Note that the -t mode does not support error reporting and does not support Bcc properly (the Bcc headers are not removed).
*
* If using -t mode, you are strongly advised to include -oi or -i in the flags (like /usr/sbin/sendmail -oi -t)
*
* -f<sender> flag will be appended automatically if one is not present.
*/
public function __construct(?string $command = null, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null)
{
parent::__construct($dispatcher, $logger);
if (null !== $command) {
if (!str_contains($command, ' -bs') && !str_contains($command, ' -t')) {
throw new \InvalidArgumentException(\sprintf('Unsupported sendmail command flags "%s"; must be one of "-bs" or "-t" but can include additional flags.', $command));
}
$this->command = $command;
}
$this->stream = new ProcessStream();
if (str_contains($this->command, ' -bs')) {
$this->stream->setCommand($this->command);
$this->stream->setInteractive(true);
$this->transport = new SmtpTransport($this->stream, $dispatcher, $logger);
}
}
public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage
{
if ($this->transport) {
return $this->transport->send($message, $envelope);
}
return parent::send($message, $envelope);
}
public function __toString(): string
{
if ($this->transport) {
return (string) $this->transport;
}
return 'smtp://sendmail';
}
protected function doSend(SentMessage $message): void
{
$this->getLogger()->debug(\sprintf('Email transport "%s" starting', __CLASS__));
$command = $this->command;
if ($recipients = $message->getEnvelope()->getRecipients()) {
$command = str_replace(' -t', '', $command);
}
if (!str_contains($command, ' -f')) {
$command .= ' -f'.escapeshellarg($message->getEnvelope()->getSender()->getEncodedAddress());
}
$chunks = AbstractStream::replace("\r\n", "\n", $message->toIterable());
if (!str_contains($command, ' -i') && !str_contains($command, ' -oi')) {
$chunks = AbstractStream::replace("\n.", "\n..", $chunks);
}
foreach ($recipients as $recipient) {
$command .= ' '.escapeshellarg($recipient->getEncodedAddress());
}
$this->stream->setCommand($command);
$this->stream->initialize();
foreach ($chunks as $chunk) {
$this->stream->write($chunk, false);
}
$this->stream->flush();
$this->stream->terminate();
$this->getLogger()->debug(\sprintf('Email transport "%s" stopped', __CLASS__));
}
}

View File

@@ -1,34 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Transport;
use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
/**
* @author Konstantin Myakshin <molodchick@gmail.com>
*/
final class SendmailTransportFactory extends AbstractTransportFactory
{
public function create(Dsn $dsn): TransportInterface
{
if ('sendmail+smtp' === $dsn->getScheme() || 'sendmail' === $dsn->getScheme()) {
return new SendmailTransport($dsn->getOption('command'), $this->dispatcher, $this->logger);
}
throw new UnsupportedSchemeException($dsn, 'sendmail', $this->getSupportedSchemes());
}
protected function getSupportedSchemes(): array
{
return ['sendmail', 'sendmail+smtp'];
}
}

View File

@@ -1,35 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Transport\Smtp\Auth;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
/**
* An Authentication mechanism.
*
* @author Chris Corbyn
*/
interface AuthenticatorInterface
{
/**
* Tries to authenticate the user.
*
* @throws TransportExceptionInterface
*/
public function authenticate(EsmtpTransport $client): void;
/**
* Gets the name of the AUTH mechanism this Authenticator handles.
*/
public function getAuthKeyword(): string;
}

View File

@@ -1,64 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Transport\Smtp\Auth;
use Symfony\Component\Mailer\Exception\InvalidArgumentException;
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
/**
* Handles CRAM-MD5 authentication.
*
* @author Chris Corbyn
*/
class CramMd5Authenticator implements AuthenticatorInterface
{
public function getAuthKeyword(): string
{
return 'CRAM-MD5';
}
/**
* @see https://www.ietf.org/rfc/rfc4954.txt
*/
public function authenticate(EsmtpTransport $client): void
{
$challenge = $client->executeCommand("AUTH CRAM-MD5\r\n", [334]);
$challenge = base64_decode(substr($challenge, 4));
$message = base64_encode($client->getUsername().' '.$this->getResponse($client->getPassword(), $challenge));
$client->executeCommand(\sprintf("%s\r\n", $message), [235]);
}
/**
* Generates a CRAM-MD5 response from a server challenge.
*/
private function getResponse(#[\SensitiveParameter] string $secret, string $challenge): string
{
if (!$secret) {
throw new InvalidArgumentException('A non-empty secret is required.');
}
if (\strlen($secret) > 64) {
$secret = pack('H32', md5($secret));
}
if (\strlen($secret) < 64) {
$secret = str_pad($secret, 64, \chr(0));
}
$kipad = substr($secret, 0, 64) ^ str_repeat(\chr(0x36), 64);
$kopad = substr($secret, 0, 64) ^ str_repeat(\chr(0x5C), 64);
$inner = pack('H32', md5($kipad.$challenge));
return md5($kopad.$inner);
}
}

View File

@@ -1,37 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Transport\Smtp\Auth;
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
/**
* Handles LOGIN authentication.
*
* @author Chris Corbyn
*/
class LoginAuthenticator implements AuthenticatorInterface
{
public function getAuthKeyword(): string
{
return 'LOGIN';
}
/**
* @see https://www.ietf.org/rfc/rfc4954.txt
*/
public function authenticate(EsmtpTransport $client): void
{
$client->executeCommand("AUTH LOGIN\r\n", [334]);
$client->executeCommand(\sprintf("%s\r\n", base64_encode($client->getUsername())), [334]);
$client->executeCommand(\sprintf("%s\r\n", base64_encode($client->getPassword())), [235]);
}
}

View File

@@ -1,35 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Transport\Smtp\Auth;
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
/**
* Handles PLAIN authentication.
*
* @author Chris Corbyn
*/
class PlainAuthenticator implements AuthenticatorInterface
{
public function getAuthKeyword(): string
{
return 'PLAIN';
}
/**
* @see https://www.ietf.org/rfc/rfc4954.txt
*/
public function authenticate(EsmtpTransport $client): void
{
$client->executeCommand(\sprintf("AUTH PLAIN %s\r\n", base64_encode($client->getUsername().\chr(0).$client->getUsername().\chr(0).$client->getPassword())), [235]);
}
}

View File

@@ -1,37 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Transport\Smtp\Auth;
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
/**
* Handles XOAUTH2 authentication.
*
* @author xu.li<AthenaLightenedMyPath@gmail.com>
*
* @see https://developers.google.com/google-apps/gmail/xoauth2_protocol
*/
class XOAuth2Authenticator implements AuthenticatorInterface
{
public function getAuthKeyword(): string
{
return 'XOAUTH2';
}
/**
* @see https://developers.google.com/google-apps/gmail/xoauth2_protocol#the_sasl_xoauth2_mechanism
*/
public function authenticate(EsmtpTransport $client): void
{
$client->executeCommand('AUTH XOAUTH2 '.base64_encode('user='.$client->getUsername()."\1auth=Bearer ".$client->getPassword()."\1\1")."\r\n", [235]);
}
}

View File

@@ -1,271 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Transport\Smtp;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Exception\TransportException;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\Exception\UnexpectedResponseException;
use Symfony\Component\Mailer\Transport\Smtp\Auth\AuthenticatorInterface;
use Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream;
use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream;
/**
* Sends Emails over SMTP with ESMTP support.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Chris Corbyn
*/
class EsmtpTransport extends SmtpTransport
{
private array $authenticators = [];
private string $username = '';
private string $password = '';
private array $capabilities;
private bool $autoTls = true;
private bool $requireTls = false;
public function __construct(string $host = 'localhost', int $port = 0, ?bool $tls = null, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null, ?AbstractStream $stream = null, ?array $authenticators = null)
{
parent::__construct($stream, $dispatcher, $logger);
if (null === $authenticators) {
// fallback to default authenticators
// order is important here (roughly most secure and popular first)
$authenticators = [
new Auth\CramMd5Authenticator(),
new Auth\LoginAuthenticator(),
new Auth\PlainAuthenticator(),
new Auth\XOAuth2Authenticator(),
];
}
$this->setAuthenticators($authenticators);
/** @var SocketStream $stream */
$stream = $this->getStream();
if (null === $tls) {
if (465 === $port) {
$tls = true;
} else {
$tls = \defined('OPENSSL_VERSION_NUMBER') && 0 === $port && 'localhost' !== $host;
}
}
if (!$tls) {
$stream->disableTls();
}
if (0 === $port) {
$port = $tls ? 465 : 25;
}
$stream->setHost($host);
$stream->setPort($port);
}
/**
* @return $this
*/
public function setUsername(string $username): static
{
$this->username = $username;
return $this;
}
public function getUsername(): string
{
return $this->username;
}
/**
* @return $this
*/
public function setPassword(#[\SensitiveParameter] string $password): static
{
$this->password = $password;
return $this;
}
public function getPassword(): string
{
return $this->password;
}
/**
* @return $this
*/
public function setAutoTls(bool $autoTls): static
{
$this->autoTls = $autoTls;
return $this;
}
public function isAutoTls(): bool
{
return $this->autoTls;
}
/**
* @return $this
*/
public function setRequireTls(bool $requireTls): static
{
$this->requireTls = $requireTls;
return $this;
}
public function isTlsRequired(): bool
{
return $this->requireTls;
}
public function setAuthenticators(array $authenticators): void
{
$this->authenticators = [];
foreach ($authenticators as $authenticator) {
$this->addAuthenticator($authenticator);
}
}
public function addAuthenticator(AuthenticatorInterface $authenticator): void
{
$this->authenticators[] = $authenticator;
}
public function executeCommand(string $command, array $codes): string
{
return [250] === $codes && str_starts_with($command, 'HELO ') ? $this->doEhloCommand() : parent::executeCommand($command, $codes);
}
final protected function getCapabilities(): array
{
return $this->capabilities;
}
private function doEhloCommand(): string
{
try {
$response = $this->executeCommand(\sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]);
} catch (TransportExceptionInterface $e) {
try {
return parent::executeCommand(\sprintf("HELO %s\r\n", $this->getLocalDomain()), [250]);
} catch (TransportExceptionInterface $ex) {
if (!$ex->getCode()) {
throw $e;
}
throw $ex;
}
}
$this->capabilities = $this->parseCapabilities($response);
/** @var SocketStream $stream */
$stream = $this->getStream();
$tlsStarted = $stream->isTls();
// WARNING: !$stream->isTLS() is right, 100% sure :)
// if you think that the ! should be removed, read the code again
// if doing so "fixes" your issue then it probably means your SMTP server behaves incorrectly or is wrongly configured
if ($this->autoTls && !$stream->isTLS() && \defined('OPENSSL_VERSION_NUMBER') && \array_key_exists('STARTTLS', $this->capabilities)) {
$this->executeCommand("STARTTLS\r\n", [220]);
if (!$stream->startTLS()) {
throw new TransportException('Unable to connect with STARTTLS.');
}
$tlsStarted = true;
$response = $this->executeCommand(\sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]);
$this->capabilities = $this->parseCapabilities($response);
}
if (!$tlsStarted && $this->isTlsRequired()) {
throw new TransportException('TLS required but neither TLS or STARTTLS are in use.');
}
if (\array_key_exists('AUTH', $this->capabilities)) {
$this->handleAuth($this->capabilities['AUTH']);
}
return $response;
}
private function parseCapabilities(string $ehloResponse): array
{
$capabilities = [];
$lines = explode("\r\n", trim($ehloResponse));
array_shift($lines);
foreach ($lines as $line) {
if (preg_match('/^[0-9]{3}[ -]([A-Z0-9-]+)((?:[ =].*)?)$/Di', $line, $matches)) {
$value = strtoupper(ltrim($matches[2], ' ='));
$capabilities[strtoupper($matches[1])] = $value ? explode(' ', $value) : [];
}
}
return $capabilities;
}
protected function serverSupportsSmtpUtf8(): bool
{
return \array_key_exists('SMTPUTF8', $this->capabilities);
}
private function handleAuth(array $modes): void
{
if (!$this->username) {
return;
}
$code = null;
$authNames = [];
$errors = [];
$modes = array_map('strtolower', $modes);
foreach ($this->authenticators as $authenticator) {
if (!\in_array(strtolower($authenticator->getAuthKeyword()), $modes, true)) {
continue;
}
$code = null;
$authNames[] = $authenticator->getAuthKeyword();
try {
$authenticator->authenticate($this);
return;
} catch (UnexpectedResponseException $e) {
$code = $e->getCode();
try {
$this->executeCommand("RSET\r\n", [250]);
} catch (TransportExceptionInterface) {
// ignore this exception as it probably means that the server error was final
}
// keep the error message, but tries the other authenticators
$errors[$authenticator->getAuthKeyword()] = $e->getMessage();
}
}
if (!$authNames) {
throw new TransportException(\sprintf('Failed to find an authenticator supported by the SMTP server, which currently supports: "%s".', implode('", "', $modes)), $code ?: 504);
}
$message = \sprintf('Failed to authenticate on SMTP server with username "%s" using the following authenticators: "%s".', $this->username, implode('", "', $authNames));
foreach ($errors as $name => $error) {
$message .= \sprintf(' Authenticator "%s" returned "%s".', $name, $error);
}
throw new TransportException($message, $code ?: 535);
}
}

View File

@@ -1,89 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Transport\Smtp;
use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
use Symfony\Component\Mailer\Transport\AbstractTransportFactory;
use Symfony\Component\Mailer\Transport\Dsn;
use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream;
use Symfony\Component\Mailer\Transport\TransportInterface;
/**
* @author Konstantin Myakshin <molodchick@gmail.com>
*/
final class EsmtpTransportFactory extends AbstractTransportFactory
{
public function create(Dsn $dsn): TransportInterface
{
if (!\in_array($dsn->getScheme(), $this->getSupportedSchemes(), true)) {
throw new UnsupportedSchemeException($dsn, 'smtp', $this->getSupportedSchemes());
}
$autoTls = '' === $dsn->getOption('auto_tls') || filter_var($dsn->getOption('auto_tls', true), \FILTER_VALIDATE_BOOL);
$tls = 'smtps' === $dsn->getScheme() ? true : ($autoTls ? null : false);
$port = $dsn->getPort(0);
$host = $dsn->getHost();
$transport = new EsmtpTransport($host, $port, $tls, $this->dispatcher, $this->logger);
$transport->setAutoTls($autoTls);
$transport->setRequireTls($dsn->getBooleanOption('require_tls'));
/** @var SocketStream $stream */
$stream = $transport->getStream();
if ('' !== $sourceIp = $dsn->getOption('source_ip', '')) {
$stream->setSourceIp($sourceIp);
}
$streamOptions = $stream->getStreamOptions();
if ('' !== $dsn->getOption('verify_peer') && !filter_var($dsn->getOption('verify_peer', true), \FILTER_VALIDATE_BOOL)) {
$streamOptions['ssl']['verify_peer'] = false;
$streamOptions['ssl']['verify_peer_name'] = false;
}
if (null !== $peerFingerprint = $dsn->getOption('peer_fingerprint')) {
$streamOptions['ssl']['peer_fingerprint'] = $peerFingerprint;
}
$stream->setStreamOptions($streamOptions);
if ($user = $dsn->getUser()) {
$transport->setUsername($user);
}
if ($password = $dsn->getPassword()) {
$transport->setPassword($password);
}
if (null !== ($localDomain = $dsn->getOption('local_domain'))) {
$transport->setLocalDomain($localDomain);
}
if (null !== ($maxPerSecond = $dsn->getOption('max_per_second'))) {
$transport->setMaxPerSecond((float) $maxPerSecond);
}
if (null !== ($restartThreshold = $dsn->getOption('restart_threshold'))) {
$transport->setRestartThreshold((int) $restartThreshold, (int) $dsn->getOption('restart_threshold_sleep', 0));
}
if (null !== ($pingThreshold = $dsn->getOption('ping_threshold'))) {
$transport->setPingThreshold((int) $pingThreshold);
}
return $transport;
}
protected function getSupportedSchemes(): array
{
return ['smtp', 'smtps'];
}
}

View File

@@ -1,393 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Transport\Smtp;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Exception\InvalidArgumentException;
use Symfony\Component\Mailer\Exception\LogicException;
use Symfony\Component\Mailer\Exception\TransportException;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\Exception\UnexpectedResponseException;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mailer\Transport\AbstractTransport;
use Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream;
use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream;
use Symfony\Component\Mime\RawMessage;
/**
* Sends emails over SMTP.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Chris Corbyn
*/
class SmtpTransport extends AbstractTransport
{
private bool $started = false;
private int $restartThreshold = 100;
private int $restartThresholdSleep = 0;
private int $restartCounter = 0;
private int $pingThreshold = 100;
private float $lastMessageTime = 0;
private AbstractStream $stream;
private string $domain = '[127.0.0.1]';
public function __construct(?AbstractStream $stream = null, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null)
{
parent::__construct($dispatcher, $logger);
$this->stream = $stream ?? new SocketStream();
}
public function getStream(): AbstractStream
{
return $this->stream;
}
/**
* Sets the maximum number of messages to send before re-starting the transport.
*
* By default, the threshold is set to 100 (and no sleep at restart).
*
* @param int $threshold The maximum number of messages (0 to disable)
* @param int $sleep The number of seconds to sleep between stopping and re-starting the transport
*
* @return $this
*/
public function setRestartThreshold(int $threshold, int $sleep = 0): static
{
$this->restartThreshold = $threshold;
$this->restartThresholdSleep = $sleep;
return $this;
}
/**
* Sets the minimum number of seconds required between two messages, before the server is pinged.
* If the transport wants to send a message and the time since the last message exceeds the specified threshold,
* the transport will ping the server first (NOOP command) to check if the connection is still alive.
* Otherwise the message will be sent without pinging the server first.
*
* Do not set the threshold too low, as the SMTP server may drop the connection if there are too many
* non-mail commands (like pinging the server with NOOP).
*
* By default, the threshold is set to 100 seconds.
*
* @param int $seconds The minimum number of seconds between two messages required to ping the server
*
* @return $this
*/
public function setPingThreshold(int $seconds): static
{
$this->pingThreshold = $seconds;
return $this;
}
/**
* Sets the name of the local domain that will be used in HELO.
*
* This should be a fully-qualified domain name and should be truly the domain
* you're using.
*
* If your server does not have a domain name, use the IP address. This will
* automatically be wrapped in square brackets as described in RFC 5321,
* section 4.1.3.
*
* @return $this
*/
public function setLocalDomain(string $domain): static
{
if ('' !== $domain && '[' !== $domain[0]) {
if (filter_var($domain, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) {
$domain = '['.$domain.']';
} elseif (filter_var($domain, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) {
$domain = '[IPv6:'.$domain.']';
}
}
$this->domain = $domain;
return $this;
}
/**
* Gets the name of the domain that will be used in HELO.
*
* If an IP address was specified, this will be returned wrapped in square
* brackets as described in RFC 5321, section 4.1.3.
*/
public function getLocalDomain(): string
{
return $this->domain;
}
public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage
{
try {
$message = parent::send($message, $envelope);
} catch (TransportExceptionInterface $e) {
if ($this->started) {
try {
$this->executeCommand("RSET\r\n", [250]);
} catch (TransportExceptionInterface) {
// ignore this exception as it probably means that the server error was final
}
}
throw $e;
}
$this->checkRestartThreshold();
return $message;
}
protected function parseMessageId(string $mtaResult): string
{
$regexps = [
'/250 Ok (?P<id>[0-9a-f-]+)\r?$/mis',
'/250 Ok:? queued as (?P<id>[A-Z0-9]+)\r?$/mis',
];
$matches = [];
foreach ($regexps as $regexp) {
if (preg_match($regexp, $mtaResult, $matches)) {
return $matches['id'];
}
}
return '';
}
public function __toString(): string
{
if ($this->stream instanceof SocketStream) {
$name = \sprintf('smtp%s://%s', ($tls = $this->stream->isTLS()) ? 's' : '', $this->stream->getHost());
$port = $this->stream->getPort();
if (!(25 === $port || ($tls && 465 === $port))) {
$name .= ':'.$port;
}
return $name;
}
return 'smtp://sendmail';
}
/**
* Runs a command against the stream, expecting the given response codes.
*
* @param int[] $codes
*
* @throws TransportException when an invalid response if received
*/
public function executeCommand(string $command, array $codes): string
{
$this->stream->write($command);
$response = $this->getFullResponse();
$this->assertResponseCode($response, $codes);
return $response;
}
protected function doSend(SentMessage $message): void
{
if (microtime(true) - $this->lastMessageTime > $this->pingThreshold) {
$this->ping();
}
try {
if (!$this->started) {
$this->start();
}
$envelope = $message->getEnvelope();
$this->doMailFromCommand($envelope->getSender()->getEncodedAddress(), $envelope->anyAddressHasUnicodeLocalpart());
foreach ($envelope->getRecipients() as $recipient) {
$this->doRcptToCommand($recipient->getEncodedAddress());
}
$this->executeCommand("DATA\r\n", [354]);
try {
foreach (AbstractStream::replace("\r\n.", "\r\n..", $message->toIterable()) as $chunk) {
$this->stream->write($chunk, false);
}
$this->stream->flush();
} catch (TransportExceptionInterface $e) {
throw $e;
} catch (\Exception $e) {
$this->stream->terminate();
$this->started = false;
$this->getLogger()->debug(\sprintf('Email transport "%s" stopped', __CLASS__));
throw $e;
}
$mtaResult = $this->executeCommand("\r\n.\r\n", [250]);
$message->appendDebug($this->stream->getDebug());
$this->lastMessageTime = microtime(true);
if ($mtaResult && $messageId = $this->parseMessageId($mtaResult)) {
$message->setMessageId($messageId);
}
} catch (TransportExceptionInterface $e) {
$e->appendDebug($this->stream->getDebug());
$this->lastMessageTime = 0;
throw $e;
}
}
protected function serverSupportsSmtpUtf8(): bool
{
return false;
}
private function doHeloCommand(): void
{
$this->executeCommand(\sprintf("HELO %s\r\n", $this->domain), [250]);
}
private function doMailFromCommand(string $address, bool $smtputf8): void
{
if ($smtputf8 && !$this->serverSupportsSmtpUtf8()) {
throw new InvalidArgumentException('Invalid addresses: non-ASCII characters not supported in local-part of email.');
}
$this->executeCommand(\sprintf("MAIL FROM:<%s>%s\r\n", $address, $smtputf8 ? ' SMTPUTF8' : ''), [250]);
}
private function doRcptToCommand(string $address): void
{
$this->executeCommand(\sprintf("RCPT TO:<%s>\r\n", $address), [250, 251, 252]);
}
public function start(): void
{
if ($this->started) {
return;
}
$this->getLogger()->debug(\sprintf('Email transport "%s" starting', __CLASS__));
$this->stream->initialize();
$this->assertResponseCode($this->getFullResponse(), [220]);
$this->doHeloCommand();
$this->started = true;
$this->lastMessageTime = 0;
$this->getLogger()->debug(\sprintf('Email transport "%s" started', __CLASS__));
}
/**
* Manually disconnect from the SMTP server.
*
* In most cases this is not necessary since the disconnect happens automatically on termination.
* In cases of long-running scripts, this might however make sense to avoid keeping an open
* connection to the SMTP server in between sending emails.
*/
public function stop(): void
{
if (!$this->started) {
return;
}
$this->getLogger()->debug(\sprintf('Email transport "%s" stopping', __CLASS__));
try {
$this->executeCommand("QUIT\r\n", [221]);
} catch (TransportExceptionInterface) {
} finally {
$this->stream->terminate();
$this->started = false;
$this->getLogger()->debug(\sprintf('Email transport "%s" stopped', __CLASS__));
}
}
private function ping(): void
{
if (!$this->started) {
return;
}
try {
$this->executeCommand("NOOP\r\n", [250]);
} catch (TransportExceptionInterface) {
$this->stop();
}
}
/**
* @throws TransportException if a response code is incorrect
*/
private function assertResponseCode(string $response, array $codes): void
{
if (!$codes) {
throw new LogicException('You must set the expected response code.');
}
[$code] = sscanf($response, '%3d');
$valid = \in_array($code, $codes);
if (!$valid || !$response) {
$codeStr = $code ? \sprintf('code "%s"', $code) : 'empty code';
$responseStr = $response ? \sprintf(', with message "%s"', trim($response)) : '';
throw new UnexpectedResponseException(\sprintf('Expected response code "%s" but got ', implode('/', $codes)).$codeStr.$responseStr.'.', $code ?: 0);
}
}
private function getFullResponse(): string
{
$response = '';
do {
$line = $this->stream->readLine();
$response .= $line;
} while ($line && isset($line[3]) && ' ' !== $line[3]);
return $response;
}
private function checkRestartThreshold(): void
{
// when using sendmail via non-interactive mode, the transport is never "started"
if (!$this->started) {
return;
}
++$this->restartCounter;
if ($this->restartCounter < $this->restartThreshold) {
return;
}
$this->stop();
if (0 < $sleep = $this->restartThresholdSleep) {
$this->getLogger()->debug(\sprintf('Email transport "%s" sleeps for %d seconds after stopping', __CLASS__, $sleep));
sleep($sleep);
}
$this->start();
$this->restartCounter = 0;
}
public function __sleep(): array
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup(): void
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
$this->stop();
}
}

View File

@@ -1,145 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Transport\Smtp\Stream;
use Symfony\Component\Mailer\Exception\TransportException;
/**
* A stream supporting remote sockets and local processes.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Nicolas Grekas <p@tchwork.com>
* @author Chris Corbyn
*
* @internal
*/
abstract class AbstractStream
{
/** @var resource|null */
protected $stream;
/** @var resource|null */
protected $in;
/** @var resource|null */
protected $out;
protected $err;
private string $debug = '';
public function write(string $bytes, bool $debug = true): void
{
if ($debug) {
$timestamp = (new \DateTimeImmutable())->format('Y-m-d\TH:i:s.up');
foreach (explode("\n", trim($bytes)) as $line) {
$this->debug .= \sprintf("[%s] > %s\n", $timestamp, $line);
}
}
$bytesToWrite = \strlen($bytes);
$totalBytesWritten = 0;
while ($totalBytesWritten < $bytesToWrite) {
$bytesWritten = @fwrite($this->in, substr($bytes, $totalBytesWritten));
if (false === $bytesWritten || 0 === $bytesWritten) {
throw new TransportException('Unable to write bytes on the wire.');
}
$totalBytesWritten += $bytesWritten;
}
}
/**
* Flushes the contents of the stream (empty it) and set the internal pointer to the beginning.
*/
public function flush(): void
{
fflush($this->in);
}
/**
* Performs any initialization needed.
*/
abstract public function initialize(): void;
public function terminate(): void
{
$this->stream = $this->err = $this->out = $this->in = null;
}
public function readLine(): string
{
if (feof($this->out)) {
return '';
}
$line = @fgets($this->out);
if ('' === $line || false === $line) {
if (stream_get_meta_data($this->out)['timed_out']) {
throw new TransportException(\sprintf('Connection to "%s" timed out.', $this->getReadConnectionDescription()));
}
if (feof($this->out)) { // don't use "eof" metadata, it's not accurate on Windows
throw new TransportException(\sprintf('Connection to "%s" has been closed unexpectedly.', $this->getReadConnectionDescription()));
}
if (false === $line) {
throw new TransportException(\sprintf('Unable to read from connection to "%s": ', $this->getReadConnectionDescription().error_get_last()['message'] ?? ''));
}
}
$this->debug .= \sprintf('[%s] < %s', (new \DateTimeImmutable())->format('Y-m-d\TH:i:s.up'), $line);
return $line;
}
public function getDebug(): string
{
$debug = $this->debug;
$this->debug = '';
return $debug;
}
public static function replace(string $from, string $to, iterable $chunks): \Generator
{
if ('' === $from) {
yield from $chunks;
return;
}
$carry = '';
$fromLen = \strlen($from);
foreach ($chunks as $chunk) {
if ('' === $chunk = $carry.$chunk) {
continue;
}
if (str_contains($chunk, $from)) {
$chunk = explode($from, $chunk);
$carry = array_pop($chunk);
yield implode($to, $chunk).$to;
} else {
$carry = $chunk;
}
if (\strlen($carry) > $fromLen) {
yield substr($carry, 0, -$fromLen);
$carry = substr($carry, -$fromLen);
}
}
if ('' !== $carry) {
yield $carry;
}
}
abstract protected function getReadConnectionDescription(): string;
}

View File

@@ -1,81 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Transport\Smtp\Stream;
use Symfony\Component\Mailer\Exception\TransportException;
/**
* A stream supporting local processes.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Chris Corbyn
*
* @internal
*/
final class ProcessStream extends AbstractStream
{
private string $command;
private bool $interactive = false;
public function setCommand(string $command): void
{
$this->command = $command;
}
public function setInteractive(bool $interactive): void
{
$this->interactive = $interactive;
}
public function initialize(): void
{
$descriptorSpec = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', '\\' === \DIRECTORY_SEPARATOR ? 'a' : 'w'],
];
$pipes = [];
$this->stream = proc_open($this->command, $descriptorSpec, $pipes);
stream_set_blocking($pipes[2], false);
if ($err = stream_get_contents($pipes[2])) {
throw new TransportException('Process could not be started: '.$err);
}
$this->in = &$pipes[0];
$this->out = &$pipes[1];
$this->err = &$pipes[2];
}
public function terminate(): void
{
if (null !== $this->stream) {
fclose($this->in);
$out = stream_get_contents($this->out);
fclose($this->out);
$err = stream_get_contents($this->err);
fclose($this->err);
if (0 !== $exitCode = proc_close($this->stream)) {
$errorMessage = 'Process failed with exit code '.$exitCode.': '.$out.$err;
}
}
parent::terminate();
if (!$this->interactive && isset($errorMessage)) {
throw new TransportException($errorMessage);
}
}
protected function getReadConnectionDescription(): string
{
return 'process '.$this->command;
}
}

View File

@@ -1,193 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Transport\Smtp\Stream;
use Symfony\Component\Mailer\Exception\TransportException;
/**
* A stream supporting remote sockets.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Chris Corbyn
*
* @internal
*/
final class SocketStream extends AbstractStream
{
private string $url;
private string $host = 'localhost';
private int $port = 465;
private float $timeout;
private bool $tls = true;
private ?string $sourceIp = null;
private array $streamContextOptions = [];
/**
* @return $this
*/
public function setTimeout(float $timeout): static
{
$this->timeout = $timeout;
return $this;
}
public function getTimeout(): float
{
return $this->timeout ?? (float) \ini_get('default_socket_timeout');
}
/**
* Literal IPv6 addresses should be wrapped in square brackets.
*
* @return $this
*/
public function setHost(string $host): static
{
$this->host = $host;
return $this;
}
public function getHost(): string
{
return $this->host;
}
/**
* @return $this
*/
public function setPort(int $port): static
{
$this->port = $port;
return $this;
}
public function getPort(): int
{
return $this->port;
}
/**
* Sets the TLS/SSL on the socket (disables STARTTLS).
*
* @return $this
*/
public function disableTls(): static
{
$this->tls = false;
return $this;
}
public function isTLS(): bool
{
return $this->tls;
}
/**
* @return $this
*/
public function setStreamOptions(array $options): static
{
$this->streamContextOptions = $options;
return $this;
}
public function getStreamOptions(): array
{
return $this->streamContextOptions;
}
/**
* Sets the source IP.
*
* IPv6 addresses should be wrapped in square brackets.
*
* @return $this
*/
public function setSourceIp(string $ip): static
{
$this->sourceIp = $ip;
return $this;
}
/**
* Returns the IP used to connect to the destination.
*/
public function getSourceIp(): ?string
{
return $this->sourceIp;
}
public function initialize(): void
{
$this->url = $this->host.':'.$this->port;
if ($this->tls) {
$this->url = 'ssl://'.$this->url;
}
$options = [];
if ($this->sourceIp) {
$options['socket']['bindto'] = $this->sourceIp.':0';
}
if ($this->streamContextOptions) {
$options = array_merge($options, $this->streamContextOptions);
}
// do it unconditionally as it will be used by STARTTLS as well if supported
$options['ssl']['crypto_method'] ??= \STREAM_CRYPTO_METHOD_TLS_CLIENT | \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
$streamContext = stream_context_create($options);
$timeout = $this->getTimeout();
set_error_handler(function ($type, $msg) {
throw new TransportException(\sprintf('Connection could not be established with host "%s": ', $this->url).$msg);
});
try {
$this->stream = stream_socket_client($this->url, $errno, $errstr, $timeout, \STREAM_CLIENT_CONNECT, $streamContext);
} finally {
restore_error_handler();
}
stream_set_blocking($this->stream, true);
stream_set_timeout($this->stream, (int) $timeout, (int) (($timeout - (int) $timeout) * 1000000));
$this->in = &$this->stream;
$this->out = &$this->stream;
}
public function startTLS(): bool
{
set_error_handler(function ($type, $msg) {
throw new TransportException('Unable to connect with STARTTLS: '.$msg);
});
try {
return stream_socket_enable_crypto($this->stream, true);
} finally {
restore_error_handler();
}
}
public function terminate(): void
{
if (null !== $this->stream) {
fclose($this->stream);
}
parent::terminate();
}
protected function getReadConnectionDescription(): string
{
return $this->url;
}
}

View File

@@ -1,29 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Transport;
use Symfony\Component\Mailer\Exception\IncompleteDsnException;
use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
/**
* @author Konstantin Myakshin <molodchick@gmail.com>
*/
interface TransportFactoryInterface
{
/**
* @throws UnsupportedSchemeException
* @throws IncompleteDsnException
*/
public function create(Dsn $dsn): TransportInterface;
public function supports(Dsn $dsn): bool;
}

View File

@@ -1,33 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Transport;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mime\RawMessage;
/**
* Interface for all mailer transports.
*
* When sending emails, you should prefer MailerInterface implementations
* as they allow asynchronous sending.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface TransportInterface extends \Stringable
{
/**
* @throws TransportExceptionInterface
*/
public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage;
}

View File

@@ -1,75 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Transport;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Exception\InvalidArgumentException;
use Symfony\Component\Mailer\Exception\LogicException;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\RawMessage;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class Transports implements TransportInterface
{
/**
* @var array<string, TransportInterface>
*/
private array $transports = [];
private TransportInterface $default;
/**
* @param iterable<string, TransportInterface> $transports
*/
public function __construct(iterable $transports)
{
foreach ($transports as $name => $transport) {
$this->default ??= $transport;
$this->transports[$name] = $transport;
}
if (!$this->transports) {
throw new LogicException(\sprintf('"%s" must have at least one transport configured.', __CLASS__));
}
}
public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage
{
/** @var Message $message */
if (RawMessage::class === $message::class || !$message->getHeaders()->has('X-Transport')) {
return $this->default->send($message, $envelope);
}
$headers = $message->getHeaders();
$transport = $headers->get('X-Transport')->getBody();
$headers->remove('X-Transport');
if (!isset($this->transports[$transport])) {
throw new InvalidArgumentException(\sprintf('The "%s" transport does not exist (available transports: "%s").', $transport, implode('", "', array_keys($this->transports))));
}
try {
return $this->transports[$transport]->send($message, $envelope);
} catch (\Throwable $e) {
$headers->addTextHeader('X-Transport', $transport);
throw $e;
}
}
public function __toString(): string
{
return '['.implode(',', array_keys($this->transports)).']';
}
}

View File

@@ -1,47 +0,0 @@
{
"name": "symfony/mailer",
"type": "library",
"description": "Helps sending emails",
"keywords": [],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=8.2",
"egulias/email-validator": "^2.1.10|^3|^4",
"psr/event-dispatcher": "^1",
"psr/log": "^1|^2|^3",
"symfony/event-dispatcher": "^6.4|^7.0",
"symfony/mime": "^7.2",
"symfony/service-contracts": "^2.5|^3"
},
"require-dev": {
"symfony/console": "^6.4|^7.0",
"symfony/http-client": "^6.4|^7.0",
"symfony/messenger": "^6.4|^7.0",
"symfony/twig-bridge": "^6.4|^7.0"
},
"conflict": {
"symfony/http-client-contracts": "<2.5",
"symfony/http-kernel": "<6.4",
"symfony/messenger": "<6.4",
"symfony/mime": "<6.4",
"symfony/twig-bridge": "<6.4"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Mailer\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev"
}

View File

@@ -1,140 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime;
use Egulias\EmailValidator\EmailValidator;
use Egulias\EmailValidator\Validation\MessageIDValidation;
use Egulias\EmailValidator\Validation\RFCValidation;
use Symfony\Component\Mime\Encoder\IdnAddressEncoder;
use Symfony\Component\Mime\Exception\InvalidArgumentException;
use Symfony\Component\Mime\Exception\LogicException;
use Symfony\Component\Mime\Exception\RfcComplianceException;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class Address
{
/**
* A regex that matches a structure like 'Name <email@address.com>'.
* It matches anything between the first < and last > as email address.
* This allows to use a single string to construct an Address, which can be convenient to use in
* config, and allows to have more readable config.
* This does not try to cover all edge cases for address.
*/
private const FROM_STRING_PATTERN = '~(?<displayName>[^<]*)<(?<addrSpec>.*)>[^>]*~';
private static EmailValidator $validator;
private static IdnAddressEncoder $encoder;
private string $address;
private string $name;
public function __construct(string $address, string $name = '')
{
if (!class_exists(EmailValidator::class)) {
throw new LogicException(\sprintf('The "%s" class cannot be used as it needs "%s". Try running "composer require egulias/email-validator".', __CLASS__, EmailValidator::class));
}
self::$validator ??= new EmailValidator();
$this->address = trim($address);
$this->name = trim(str_replace(["\n", "\r"], '', $name));
if (!self::$validator->isValid($this->address, class_exists(MessageIDValidation::class) ? new MessageIDValidation() : new RFCValidation())) {
throw new RfcComplianceException(\sprintf('Email "%s" does not comply with addr-spec of RFC 2822.', $address));
}
}
public function getAddress(): string
{
return $this->address;
}
public function getName(): string
{
return $this->name;
}
public function getEncodedAddress(): string
{
self::$encoder ??= new IdnAddressEncoder();
return self::$encoder->encodeString($this->address);
}
public function toString(): string
{
return ($n = $this->getEncodedName()) ? $n.' <'.$this->getEncodedAddress().'>' : $this->getEncodedAddress();
}
public function getEncodedName(): string
{
if ('' === $this->getName()) {
return '';
}
return \sprintf('"%s"', preg_replace('/"/u', '\"', $this->getName()));
}
public static function create(self|string $address): self
{
if ($address instanceof self) {
return $address;
}
if (!str_contains($address, '<')) {
return new self($address);
}
if (!preg_match(self::FROM_STRING_PATTERN, $address, $matches)) {
throw new InvalidArgumentException(\sprintf('Could not parse "%s" to a "%s" instance.', $address, self::class));
}
return new self($matches['addrSpec'], trim($matches['displayName'], ' \'"'));
}
/**
* @param array<Address|string> $addresses
*
* @return Address[]
*/
public static function createArray(array $addresses): array
{
$addrs = [];
foreach ($addresses as $address) {
$addrs[] = self::create($address);
}
return $addrs;
}
/**
* Returns true if this address' localpart contains at least one
* non-ASCII character, and false if it is only ASCII (or empty).
*
* This is a helper for Envelope, which has to decide whether to
* the SMTPUTF8 extensions (RFC 6530 and following) for any given
* message.
*
* The SMTPUTF8 extension is strictly required if any address
* contains a non-ASCII character in its localpart. If non-ASCII
* is only used in domains (e.g. horst@freiherr-von-mühlhausen.de)
* then it is possible to send the message using IDN encoding
* instead of SMTPUTF8. The most common software will display the
* message as intended.
*/
public function hasUnicodeLocalpart(): bool
{
return (bool) preg_match('/[\x80-\xFF].*@/', $this->address);
}
}

View File

@@ -1,20 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
interface BodyRendererInterface
{
public function render(Message $message): void;
}

View File

@@ -1,57 +0,0 @@
CHANGELOG
=========
7.0
---
* Remove `Email::attachPart()`, use `Email::addPart()` instead
* Argument `$body` is now required (at least null) in `Message::setBody()`
* Require explicit argument when calling `Message::setBody()`
6.3
---
* Support detection of related parts if `Content-Id` is used instead of the name
* Add `TextPart::getDisposition()`
6.2
---
* Add `File`
* Deprecate `Email::attachPart()`, use `addPart()` instead
* Deprecate calling `Message::setBody()` without arguments
6.1
---
* Add `DataPart::getFilename()` and `DataPart::getContentType()`
6.0
---
* Remove `Address::fromString()`, use `Address::create()` instead
* Remove `Serializable` interface from `RawMessage`
5.2.0
-----
* Add support for DKIM
* Deprecated `Address::fromString()`, use `Address::create()` instead
4.4.0
-----
* [BC BREAK] Removed `NamedAddress` (`Address` now supports a name)
* Added PHPUnit constraints
* Added `AbstractPart::asDebugString()`
* Added `Address::fromString()`
4.3.3
-----
* [BC BREAK] Renamed method `Headers::getAll()` to `Headers::all()`.
4.3.0
-----
* Introduced the component as experimental

View File

@@ -1,211 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Xavier De Cock <xdecock@gmail.com>
*
* @internal
*/
final class CharacterStream
{
/** Pre-computed for optimization */
private const UTF8_LENGTH_MAP = [
"\x00" => 1, "\x01" => 1, "\x02" => 1, "\x03" => 1, "\x04" => 1, "\x05" => 1, "\x06" => 1, "\x07" => 1,
"\x08" => 1, "\x09" => 1, "\x0a" => 1, "\x0b" => 1, "\x0c" => 1, "\x0d" => 1, "\x0e" => 1, "\x0f" => 1,
"\x10" => 1, "\x11" => 1, "\x12" => 1, "\x13" => 1, "\x14" => 1, "\x15" => 1, "\x16" => 1, "\x17" => 1,
"\x18" => 1, "\x19" => 1, "\x1a" => 1, "\x1b" => 1, "\x1c" => 1, "\x1d" => 1, "\x1e" => 1, "\x1f" => 1,
"\x20" => 1, "\x21" => 1, "\x22" => 1, "\x23" => 1, "\x24" => 1, "\x25" => 1, "\x26" => 1, "\x27" => 1,
"\x28" => 1, "\x29" => 1, "\x2a" => 1, "\x2b" => 1, "\x2c" => 1, "\x2d" => 1, "\x2e" => 1, "\x2f" => 1,
"\x30" => 1, "\x31" => 1, "\x32" => 1, "\x33" => 1, "\x34" => 1, "\x35" => 1, "\x36" => 1, "\x37" => 1,
"\x38" => 1, "\x39" => 1, "\x3a" => 1, "\x3b" => 1, "\x3c" => 1, "\x3d" => 1, "\x3e" => 1, "\x3f" => 1,
"\x40" => 1, "\x41" => 1, "\x42" => 1, "\x43" => 1, "\x44" => 1, "\x45" => 1, "\x46" => 1, "\x47" => 1,
"\x48" => 1, "\x49" => 1, "\x4a" => 1, "\x4b" => 1, "\x4c" => 1, "\x4d" => 1, "\x4e" => 1, "\x4f" => 1,
"\x50" => 1, "\x51" => 1, "\x52" => 1, "\x53" => 1, "\x54" => 1, "\x55" => 1, "\x56" => 1, "\x57" => 1,
"\x58" => 1, "\x59" => 1, "\x5a" => 1, "\x5b" => 1, "\x5c" => 1, "\x5d" => 1, "\x5e" => 1, "\x5f" => 1,
"\x60" => 1, "\x61" => 1, "\x62" => 1, "\x63" => 1, "\x64" => 1, "\x65" => 1, "\x66" => 1, "\x67" => 1,
"\x68" => 1, "\x69" => 1, "\x6a" => 1, "\x6b" => 1, "\x6c" => 1, "\x6d" => 1, "\x6e" => 1, "\x6f" => 1,
"\x70" => 1, "\x71" => 1, "\x72" => 1, "\x73" => 1, "\x74" => 1, "\x75" => 1, "\x76" => 1, "\x77" => 1,
"\x78" => 1, "\x79" => 1, "\x7a" => 1, "\x7b" => 1, "\x7c" => 1, "\x7d" => 1, "\x7e" => 1, "\x7f" => 1,
"\x80" => 0, "\x81" => 0, "\x82" => 0, "\x83" => 0, "\x84" => 0, "\x85" => 0, "\x86" => 0, "\x87" => 0,
"\x88" => 0, "\x89" => 0, "\x8a" => 0, "\x8b" => 0, "\x8c" => 0, "\x8d" => 0, "\x8e" => 0, "\x8f" => 0,
"\x90" => 0, "\x91" => 0, "\x92" => 0, "\x93" => 0, "\x94" => 0, "\x95" => 0, "\x96" => 0, "\x97" => 0,
"\x98" => 0, "\x99" => 0, "\x9a" => 0, "\x9b" => 0, "\x9c" => 0, "\x9d" => 0, "\x9e" => 0, "\x9f" => 0,
"\xa0" => 0, "\xa1" => 0, "\xa2" => 0, "\xa3" => 0, "\xa4" => 0, "\xa5" => 0, "\xa6" => 0, "\xa7" => 0,
"\xa8" => 0, "\xa9" => 0, "\xaa" => 0, "\xab" => 0, "\xac" => 0, "\xad" => 0, "\xae" => 0, "\xaf" => 0,
"\xb0" => 0, "\xb1" => 0, "\xb2" => 0, "\xb3" => 0, "\xb4" => 0, "\xb5" => 0, "\xb6" => 0, "\xb7" => 0,
"\xb8" => 0, "\xb9" => 0, "\xba" => 0, "\xbb" => 0, "\xbc" => 0, "\xbd" => 0, "\xbe" => 0, "\xbf" => 0,
"\xc0" => 2, "\xc1" => 2, "\xc2" => 2, "\xc3" => 2, "\xc4" => 2, "\xc5" => 2, "\xc6" => 2, "\xc7" => 2,
"\xc8" => 2, "\xc9" => 2, "\xca" => 2, "\xcb" => 2, "\xcc" => 2, "\xcd" => 2, "\xce" => 2, "\xcf" => 2,
"\xd0" => 2, "\xd1" => 2, "\xd2" => 2, "\xd3" => 2, "\xd4" => 2, "\xd5" => 2, "\xd6" => 2, "\xd7" => 2,
"\xd8" => 2, "\xd9" => 2, "\xda" => 2, "\xdb" => 2, "\xdc" => 2, "\xdd" => 2, "\xde" => 2, "\xdf" => 2,
"\xe0" => 3, "\xe1" => 3, "\xe2" => 3, "\xe3" => 3, "\xe4" => 3, "\xe5" => 3, "\xe6" => 3, "\xe7" => 3,
"\xe8" => 3, "\xe9" => 3, "\xea" => 3, "\xeb" => 3, "\xec" => 3, "\xed" => 3, "\xee" => 3, "\xef" => 3,
"\xf0" => 4, "\xf1" => 4, "\xf2" => 4, "\xf3" => 4, "\xf4" => 4, "\xf5" => 4, "\xf6" => 4, "\xf7" => 4,
"\xf8" => 5, "\xf9" => 5, "\xfa" => 5, "\xfb" => 5, "\xfc" => 6, "\xfd" => 6, "\xfe" => 0, "\xff" => 0,
];
private string $data = '';
private int $dataSize = 0;
private array $map = [];
private int $charCount = 0;
private int $currentPos = 0;
private int $fixedWidth = 0;
/**
* @param resource|string $input
*/
public function __construct($input, ?string $charset = 'utf-8')
{
$charset = strtolower(trim($charset)) ?: 'utf-8';
if ('utf-8' === $charset || 'utf8' === $charset) {
$this->fixedWidth = 0;
$this->map = ['p' => [], 'i' => []];
} else {
$this->fixedWidth = match ($charset) {
// 16 bits
'ucs2',
'ucs-2',
'utf16',
'utf-16' => 2,
// 32 bits
'ucs4',
'ucs-4',
'utf32',
'utf-32' => 4,
// 7-8 bit charsets: (us-)?ascii, (iso|iec)-?8859-?[0-9]+, windows-?125[0-9], cp-?[0-9]+, ansi, macintosh,
// koi-?7, koi-?8-?.+, mik, (cork|t1), v?iscii
// and fallback
default => 1,
};
}
if (\is_resource($input)) {
$blocks = 16372;
while (false !== $read = fread($input, $blocks)) {
$this->write($read);
}
} else {
$this->write($input);
}
}
public function read(int $length): ?string
{
if ($this->currentPos >= $this->charCount) {
return null;
}
$length = ($this->currentPos + $length > $this->charCount) ? $this->charCount - $this->currentPos : $length;
if ($this->fixedWidth > 0) {
$len = $length * $this->fixedWidth;
$ret = substr($this->data, $this->currentPos * $this->fixedWidth, $len);
$this->currentPos += $length;
} else {
$end = $this->currentPos + $length;
$end = min($end, $this->charCount);
$ret = '';
$start = 0;
if ($this->currentPos > 0) {
$start = $this->map['p'][$this->currentPos - 1];
}
$to = $start;
for (; $this->currentPos < $end; ++$this->currentPos) {
if (isset($this->map['i'][$this->currentPos])) {
$ret .= substr($this->data, $start, $to - $start).'?';
$start = $this->map['p'][$this->currentPos];
} else {
$to = $this->map['p'][$this->currentPos];
}
}
$ret .= substr($this->data, $start, $to - $start);
}
return $ret;
}
public function readBytes(int $length): ?array
{
if (null !== $read = $this->read($length)) {
return array_map('ord', str_split($read, 1));
}
return null;
}
public function setPointer(int $charOffset): void
{
if ($this->charCount < $charOffset) {
$charOffset = $this->charCount;
}
$this->currentPos = $charOffset;
}
public function write(string $chars): void
{
$ignored = '';
$this->data .= $chars;
if ($this->fixedWidth > 0) {
$strlen = \strlen($chars);
$ignoredL = $strlen % $this->fixedWidth;
$ignored = $ignoredL ? substr($chars, -$ignoredL) : '';
$this->charCount += ($strlen - $ignoredL) / $this->fixedWidth;
} else {
$this->charCount += $this->getUtf8CharPositions($chars, $this->dataSize, $ignored);
}
$this->dataSize = \strlen($this->data) - \strlen($ignored);
}
private function getUtf8CharPositions(string $string, int $startOffset, string &$ignoredChars): int
{
$strlen = \strlen($string);
$charPos = \count($this->map['p']);
$foundChars = 0;
$invalid = false;
for ($i = 0; $i < $strlen; ++$i) {
$char = $string[$i];
$size = self::UTF8_LENGTH_MAP[$char];
if (0 == $size) {
/* char is invalid, we must wait for a resync */
$invalid = true;
continue;
}
if ($invalid) {
/* We mark the chars as invalid and start a new char */
$this->map['p'][$charPos + $foundChars] = $startOffset + $i;
$this->map['i'][$charPos + $foundChars] = true;
++$foundChars;
$invalid = false;
}
if (($i + $size) > $strlen) {
$ignoredChars = substr($string, $i);
break;
}
for ($j = 1; $j < $size; ++$j) {
$char = $string[$i + $j];
if ($char > "\x7F" && $char < "\xC0") {
// Valid - continue parsing
} else {
/* char is invalid, we must wait for a resync */
$invalid = true;
continue 2;
}
}
/* Ok we got a complete char here */
$this->map['p'][$charPos + $foundChars] = $startOffset + $i + $size;
$i += $j - 1;
++$foundChars;
}
return $foundChars;
}
}

View File

@@ -1,97 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Crypto;
/**
* A helper providing autocompletion for available DkimSigner options.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class DkimOptions
{
private array $options = [];
public function toArray(): array
{
return $this->options;
}
/**
* @return $this
*/
public function algorithm(string $algo): static
{
$this->options['algorithm'] = $algo;
return $this;
}
/**
* @return $this
*/
public function signatureExpirationDelay(int $show): static
{
$this->options['signature_expiration_delay'] = $show;
return $this;
}
/**
* @return $this
*/
public function bodyMaxLength(int $max): static
{
$this->options['body_max_length'] = $max;
return $this;
}
/**
* @return $this
*/
public function bodyShowLength(bool $show): static
{
$this->options['body_show_length'] = $show;
return $this;
}
/**
* @return $this
*/
public function headerCanon(string $canon): static
{
$this->options['header_canon'] = $canon;
return $this;
}
/**
* @return $this
*/
public function bodyCanon(string $canon): static
{
$this->options['body_canon'] = $canon;
return $this;
}
/**
* @return $this
*/
public function headersToIgnore(array $headers): static
{
$this->options['headers_to_ignore'] = $headers;
return $this;
}
}

View File

@@ -1,217 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Crypto;
use Symfony\Component\Mime\Exception\InvalidArgumentException;
use Symfony\Component\Mime\Exception\RuntimeException;
use Symfony\Component\Mime\Header\UnstructuredHeader;
use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\Part\AbstractPart;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* RFC 6376 and 8301
*/
final class DkimSigner
{
public const CANON_SIMPLE = 'simple';
public const CANON_RELAXED = 'relaxed';
public const ALGO_SHA256 = 'rsa-sha256';
public const ALGO_ED25519 = 'ed25519-sha256'; // RFC 8463
private \OpenSSLAsymmetricKey $key;
/**
* @param string $pk The private key as a string or the path to the file containing the private key, should be prefixed with file:// (in PEM format)
* @param string $passphrase A passphrase of the private key (if any)
*/
public function __construct(
string $pk,
private string $domainName,
private string $selector,
private array $defaultOptions = [],
string $passphrase = '',
) {
if (!\extension_loaded('openssl')) {
throw new \LogicException('PHP extension "openssl" is required to use DKIM.');
}
$this->key = openssl_pkey_get_private($pk, $passphrase) ?: throw new InvalidArgumentException('Unable to load DKIM private key: '.openssl_error_string());
$this->defaultOptions += [
'algorithm' => self::ALGO_SHA256,
'signature_expiration_delay' => 0,
'body_max_length' => \PHP_INT_MAX,
'body_show_length' => false,
'header_canon' => self::CANON_RELAXED,
'body_canon' => self::CANON_RELAXED,
'headers_to_ignore' => [],
];
}
public function sign(Message $message, array $options = []): Message
{
$options += $this->defaultOptions;
if (!\in_array($options['algorithm'], [self::ALGO_SHA256, self::ALGO_ED25519], true)) {
throw new InvalidArgumentException(\sprintf('Invalid DKIM signing algorithm "%s".', $options['algorithm']));
}
$headersToIgnore['return-path'] = true;
$headersToIgnore['x-transport'] = true;
foreach ($options['headers_to_ignore'] as $name) {
$headersToIgnore[strtolower($name)] = true;
}
unset($headersToIgnore['from']);
$signedHeaderNames = [];
$headerCanonData = '';
$headers = $message->getPreparedHeaders();
foreach ($headers->getNames() as $name) {
foreach ($headers->all($name) as $header) {
if (isset($headersToIgnore[strtolower($header->getName())])) {
continue;
}
if ('' !== $header->getBodyAsString()) {
$headerCanonData .= $this->canonicalizeHeader($header->toString(), $options['header_canon']);
$signedHeaderNames[] = $header->getName();
}
}
}
[$bodyHash, $bodyLength] = $this->hashBody($message->getBody(), $options['body_canon'], $options['body_max_length']);
$params = [
'v' => '1',
'q' => 'dns/txt',
'a' => $options['algorithm'],
'bh' => base64_encode($bodyHash),
'd' => $this->domainName,
'h' => implode(': ', $signedHeaderNames),
'i' => '@'.$this->domainName,
's' => $this->selector,
't' => time(),
'c' => $options['header_canon'].'/'.$options['body_canon'],
];
if ($options['body_show_length']) {
$params['l'] = $bodyLength;
}
if ($options['signature_expiration_delay']) {
$params['x'] = $params['t'] + $options['signature_expiration_delay'];
}
$value = '';
foreach ($params as $k => $v) {
$value .= $k.'='.$v.'; ';
}
$value = trim($value);
$header = new UnstructuredHeader('DKIM-Signature', $value);
$headerCanonData .= rtrim($this->canonicalizeHeader($header->toString()."\r\n b=", $options['header_canon']));
if (self::ALGO_SHA256 === $options['algorithm']) {
if (!openssl_sign($headerCanonData, $signature, $this->key, \OPENSSL_ALGO_SHA256)) {
throw new RuntimeException('Unable to sign DKIM hash: '.openssl_error_string());
}
} else {
throw new \RuntimeException(\sprintf('The "%s" DKIM signing algorithm is not supported yet.', self::ALGO_ED25519));
}
$header->setValue($value.' b='.trim(chunk_split(base64_encode($signature), 73, ' ')));
$headers->add($header);
return new Message($headers, $message->getBody());
}
private function canonicalizeHeader(string $header, string $headerCanon): string
{
if (self::CANON_RELAXED !== $headerCanon) {
return $header."\r\n";
}
$exploded = explode(':', $header, 2);
$name = strtolower(trim($exploded[0]));
$value = str_replace("\r\n", '', $exploded[1]);
$value = trim(preg_replace("/[ \t][ \t]+/", ' ', $value));
return $name.':'.$value."\r\n";
}
private function hashBody(AbstractPart $body, string $bodyCanon, int $maxLength): array
{
$hash = hash_init('sha256');
$relaxed = self::CANON_RELAXED === $bodyCanon;
$currentLine = '';
$emptyCounter = 0;
$isSpaceSequence = false;
$length = 0;
foreach ($body->bodyToIterable() as $chunk) {
$canon = '';
for ($i = 0, $len = \strlen($chunk); $i < $len; ++$i) {
switch ($chunk[$i]) {
case "\r":
break;
case "\n":
// previous char is always \r
if ($relaxed) {
$isSpaceSequence = false;
}
if ('' === $currentLine) {
++$emptyCounter;
} else {
$currentLine = '';
$canon .= "\r\n";
}
break;
case ' ':
case "\t":
if ($relaxed) {
$isSpaceSequence = true;
break;
}
// no break
default:
if ($emptyCounter > 0) {
$canon .= str_repeat("\r\n", $emptyCounter);
$emptyCounter = 0;
}
if ($isSpaceSequence) {
$currentLine .= ' ';
$canon .= ' ';
$isSpaceSequence = false;
}
$currentLine .= $chunk[$i];
$canon .= $chunk[$i];
}
}
if ($length + \strlen($canon) >= $maxLength) {
$canon = substr($canon, 0, $maxLength - $length);
$length += \strlen($canon);
hash_update($hash, $canon);
break;
}
$length += \strlen($canon);
hash_update($hash, $canon);
}
// Add trailing Line return if last line is non empty
if ('' !== $currentLine) {
hash_update($hash, "\r\n");
$length += \strlen("\r\n");
}
if (!$relaxed && 0 === $length) {
hash_update($hash, "\r\n");
$length = 2;
}
return [hash_final($hash, true), $length];
}
}

Some files were not shown because too many files have changed in this diff Show More