Lines 100.00% 50 / 50
Methods 100.00% 4 / 4
Classes 100.00% 1 / 1
Covered by tests of size
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 send 100.00% 39 / 39 100.00% 1 / 1 6
 parseRecipients 100.00% 6 / 6 100.00% 1 / 1 3
 extractEmailAddress 100.00% 4 / 4 100.00% 1 / 1 2
29final readonly class SendGridProvider implements MailProviderInterface
30{
31    private HttpClientInterface $httpClient;
32
33    public function __construct(
34        private Configuration $configuration,
35        ?HttpClientInterface $httpClient = null,
36    ) {
37        $this->httpClient = $httpClient ?? HttpClient::create();
38    }
39
40    /**
41     * @param array<string, int|string|null> $headers
42     * @throws Exception
43     * @throws TransportExceptionInterface
44     */
45    public function send(string $recipients, array $headers, string $body): int
46    {
47        $apiKey = (string) ($this->configuration->get('mail.sendgridApiKey') ?? '');
48        if ($apiKey === '') {
49            throw new Exception('SendGrid API key is not configured.');
50        }
51
52        $fromAddress = $this->extractEmailAddress((string) ($headers['From'] ?? ''));
53        if ($fromAddress === '') {
54            throw new Exception('Missing valid From header for SendGrid provider.');
55        }
56
57        $toAddresses = $this->parseRecipients($recipients);
58        if ($toAddresses === []) {
59            throw new Exception('No valid recipients for SendGrid provider.');
60        }
61
62        $payload = [
63            'personalizations' => [
64                [
65                    'to' => array_map(static fn(string $address): array => ['email' => $address], $toAddresses),
66                    'subject' => $headers['Subject'] ?? '',
67                ],
68            ],
69            'from' => [
70                'email' => $fromAddress,
71            ],
72            'content' => [
73                ['type' => 'text/plain', 'value' => $body],
74                ['type' => 'text/html', 'value' => $body],
75            ],
76        ];
77
78        $response = $this->httpClient->request('POST', 'https://api.sendgrid.com/v3/mail/send', [
79            'headers' => [
80                'Authorization' => 'Bearer ' . $apiKey,
81                'Content-Type' => 'application/json',
82            ],
83            'json' => $payload,
84        ]);
85
86        $statusCode = $response->getStatusCode();
87        if ($statusCode < 200 || $statusCode >= 300) {
88            throw new Exception(sprintf(
89                'SendGrid request failed with status %d: %s',
90                $statusCode,
91                $response->getContent(false),
92            ));
93        }
94
95        return count($toAddresses);
96    }
97
98    /**
99     * @return array<int, string>
100     */
101    private function parseRecipients(string $recipients): array
102    {
103        $addresses = [];
104        foreach (explode(',', $recipients) as $recipient) {
105            $address = $this->extractEmailAddress($recipient);
106            if ($address !== '') {
107                $addresses[] = $address;
108            }
109        }
110
111        return array_values(array_unique($addresses));
112    }
113
114    private function extractEmailAddress(string $rawAddress): string
115    {
116        $matches = [];
117        if (preg_match('/<([^>]+)>/', $rawAddress, $matches) === 1) {
118            return trim($matches[1]);
119        }
120
121        return trim($rawAddress);
122    }
123}