Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.21% covered (warning)
84.21%
16 / 19
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
Transports
84.21% covered (warning)
84.21%
16 / 19
50.00% covered (danger)
50.00%
2 / 4
10.39
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 has
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 send
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 transports
83.33% covered (warning)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
5.12
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Mail;
6
7use Closure;
8use Cosray\Config;
9use Cosray\Exception\RuntimeException;
10use Psr\Log\LoggerInterface as Logger;
11use Symfony\Component\EventDispatcher\EventDispatcher;
12use Symfony\Component\Mailer\EventListener\EnvelopeListener;
13use Symfony\Component\Mailer\Transport;
14use Symfony\Component\Mailer\Transport\TransportInterface;
15use Symfony\Component\Mime\Email;
16
17/**
18 * The configured Symfony Mailer transports, built on first use. With
19 * `mail.redirect`, every mail goes to those addresses instead of its
20 * recipients; its headers stay as they are.
21 *
22 * @internal
23 */
24final class Transports
25{
26    private ?TransportInterface $transports = null;
27
28    /**
29     * @param ?Closure(array<non-empty-string, non-empty-string>, EventDispatcher): TransportInterface $build
30     *        builds the transports instead of the DSNs, for tests; the dispatcher carries the redirect
31     */
32    public function __construct(
33        private readonly Config $config,
34        private readonly Logger $logger,
35        private readonly ?Closure $build = null,
36    ) {}
37
38    public function has(string $name): bool
39    {
40        return array_key_exists($name, $this->config->mail->transports);
41    }
42
43    /** @param ?string $name the transport to send through, the default one if null */
44    public function send(Email $email, ?string $name = null): void
45    {
46        if ($name !== null) {
47            if (!$this->has($name)) {
48                throw new RuntimeException("There is no mail transport named {$name}.");
49            }
50
51            $email->getHeaders()->addTextHeader('X-Transport', $name);
52        }
53
54        $this->transports()->send($email);
55    }
56
57    private function transports(): TransportInterface
58    {
59        if ($this->transports !== null) {
60            return $this->transports;
61        }
62
63        $dsns = $this->config->mail->transports;
64
65        if ($dsns === []) {
66            throw new RuntimeException('No mail transport is configured: set MAILER_DSN or `mail.transports`.');
67        }
68
69        $dispatcher = new EventDispatcher();
70        $redirect = $this->config->mail->redirect;
71
72        if ($redirect !== []) {
73            $dispatcher->addSubscriber(new EnvelopeListener(recipients: $redirect));
74        }
75
76        return $this->transports = $this->build !== null
77            ? ($this->build)($dsns, $dispatcher)
78            : Transport::fromDsns($dsns, $dispatcher, logger: $this->logger);
79    }
80}