Completed
Pull Request — master (#18)
by Chris
01:24
created

SendGridCourier::prepareEmail()   B

Complexity

Conditions 6
Paths 32

Size

Total Lines 41
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 41
ccs 23
cts 23
cp 1
rs 8.439
c 0
b 0
f 0
cc 6
eloc 22
nc 32
nop 1
crap 6
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Courier;
6
7
use Courier\Exceptions\TransmissionException;
8
use Courier\Exceptions\UnsupportedContentException;
9
use Exception;
10
use PhpEmail\Address;
11
use PhpEmail\Attachment;
12
use PhpEmail\Content;
13
use PhpEmail\Email;
14
use Psr\Log\LoggerInterface;
15
use Psr\Log\NullLogger;
16
use SendGrid;
17
18
/**
19
 * A courier implementation using the SendGrid v3 web API and sendgrid-php library to send emails.
20
 *
21
 * While SendGrid supports sending batches of emails using "personalizations", this does not fit completely into the
22
 * paradigm of transactional emails. For this reason, this courier only creates a single personalization with multiple
23
 * recipients.
24
 */
25
class SendGridCourier implements ConfirmingCourier
26
{
27
    use SavesReceipts;
28
29
    /**
30
     * @var SendGrid
31
     */
32
    private $sendGrid;
33
34
    /**
35
     * @var LoggerInterface
36
     */
37
    private $logger;
38
39
    /**
40
     * @param SendGrid        $sendGrid
41
     * @param LoggerInterface $logger
42
     */
43 10
    public function __construct(SendGrid $sendGrid, LoggerInterface $logger = null)
44
    {
45 10
        $this->sendGrid = $sendGrid;
46 10
        $this->logger   = $logger ?: new NullLogger();
47
    }
48
49
    /**
50
     * {@inheritdoc}
51
     */
52 10
    public function deliver(Email $email): void
53
    {
54 10
        if (!$this->supportsContent($email->getContent())) {
55 1
            throw new UnsupportedContentException($email->getContent());
56
        }
57
58 9
        $mail = $this->prepareEmail($email);
59
60
        switch (true) {
61 9
            case $email->getContent() instanceof Content\EmptyContent:
62 4
                $response = $this->sendEmptyContent($mail);
63 2
                break;
64
65 5
            case $email->getContent() instanceof Content\Contracts\SimpleContent:
66 4
                $response = $this->sendSimpleContent($mail, $email->getContent());
67 4
                break;
68
69 1
            case $email->getContent() instanceof Content\Contracts\TemplatedContent:
70 1
                $response = $this->sendTemplatedContent($mail, $email->getContent());
71 1
                break;
72
73
            default:
74
                // Should never get here
75
                // @codeCoverageIgnoreStart
76
                throw new UnsupportedContentException($email->getContent());
77
                // @codeCoverageIgnoreEnd
78
        }
79
80 7
        $this->saveReceipt($email, $this->getReceipt($response));
81
    }
82
83 7
    protected function getReceipt(SendGrid\Response $response): string
84
    {
85 7
        $key = 'X-Message-Id';
86
87 7
        foreach ($response->headers() as $header) {
88 6
            $parts = explode(':', $header, 2);
89
90 6
            if ($parts[0] === $key) {
91 6
                return $parts[1];
92
            }
93
        }
94
95 1
        throw new TransmissionException();
96
    }
97
98
    /**
99
     * {@inheritdoc}
100
     */
101 10
    protected function supportedContent(): array
102
    {
103
        return [
104 10
            Content\EmptyContent::class,
105
            Content\Contracts\SimpleContent::class,
106
            Content\Contracts\TemplatedContent::class,
107
        ];
108
    }
109
110
    /**
111
     * Determine if the content is supported by this courier.
112
     *
113
     * @param Content $content
114
     *
115
     * @return bool
116
     */
117 10
    protected function supportsContent(Content $content): bool
118
    {
119 10
        foreach ($this->supportedContent() as $contentType) {
120 10
            if ($content instanceof $contentType) {
121 10
                return true;
122
            }
123
        }
124
125 1
        return false;
126
    }
127
128
    /**
129
     * SendGrid does not support having the same, case-insensitive email address in recipient blocks. This
130
     * function allows for filtering out non-distinct email addresses.
131
     *
132
     * @param array $emails
133
     * @param array $existing
134
     *
135
     * @return array
136
     */
137 9
    protected function distinctAddresses(array $emails, array $existing = []): array
138
    {
139 9
        $insensitiveAddresses = [];
140
141 9
        $emails = array_filter($emails, function (Address $address) use (&$insensitiveAddresses) {
142 9
            if (!in_array(strtolower($address->getEmail()), $insensitiveAddresses)) {
143 9
                $insensitiveAddresses[] = strtolower($address->getEmail());
144
145 9
                return true;
146
            }
147
148 1
            return false;
149 9
        });
150
151 9
        $existingEmails = array_map(function (Address $address) {
152 9
            return $address->getEmail();
153 9
        }, $existing);
154
155 9
        return array_filter($emails, function (Address $address) use ($existingEmails) {
156 9
            return !in_array($address->getEmail(), $existingEmails);
157 9
        });
158
    }
159
160
    /**
161
     * @param Email $email
162
     *
163
     * @return SendGrid\Mail
164
     */
165 9
    protected function prepareEmail(Email $email): SendGrid\Mail
166
    {
167 9
        $message = new SendGrid\Mail();
168
169 9
        $message->setSubject($email->getSubject());
170 9
        $message->setFrom(new SendGrid\Email($email->getFrom()->getName(), $email->getFrom()->getEmail()));
171
172 9
        $personalization = new SendGrid\Personalization();
173
174 9
        foreach ($this->distinctAddresses($email->getToRecipients()) as $recipient) {
175 9
            $personalization->addTo(new SendGrid\Email($recipient->getName(), $recipient->getEmail()));
176
        }
177
178 9
        $existingAddresses = $email->getToRecipients();
179 9
        foreach ($this->distinctAddresses($email->getCcRecipients(), $existingAddresses) as $recipient) {
180 2
            $personalization->addCc(new SendGrid\Email($recipient->getName(), $recipient->getEmail()));
181
        }
182
183 9
        $existingAddresses = array_merge($email->getToRecipients(), $email->getCcRecipients());
184 9
        foreach ($this->distinctAddresses($email->getBccRecipients(), $existingAddresses) as $recipient) {
185 2
            $personalization->addBcc(new SendGrid\Email($recipient->getName(), $recipient->getEmail()));
186
        }
187
188 9
        $message->addPersonalization($personalization);
189
190 9
        if (!empty($email->getReplyTos())) {
191
            // The SendGrid API only supports one "Reply To" :(
192 1
            $replyTos = $email->getReplyTos();
193 1
            $first    = reset($replyTos);
194 1
            $replyTo  = new SendGrid\Email($first->getName(), $first->getEmail());
195
196 1
            $message->setReplyTo($replyTo);
197
        }
198
199 9
        $message->attachments = $this->buildAttachments($email);
200
201 9
        foreach ($email->getHeaders() as $header) {
202 1
            $message->addHeader($header->getField(), $header->getValue());
203
        }
204
205 9
        return $message;
206
    }
207
208
    /**
209
     * @param SendGrid\Mail $email
210
     *
211
     * @return SendGrid\Response
212
     */
213 9
    protected function send(SendGrid\Mail $email): SendGrid\Response
214
    {
215
        try {
216
            /** @var SendGrid\Response $response */
217 9
            $response = $this->sendGrid->client->mail()->send()->post($email);
218
219 8
            if ($response->statusCode() >= 400) {
220 1
                $this->logger->error(
221 1
                    'Received status {code} from SendGrid with body: {body}',
222
                    [
223 1
                        'code' => $response->statusCode(),
224 1
                        'body' => $response->body(),
225
                    ]
226
                );
227
228 1
                throw new TransmissionException($response->statusCode());
229
            }
230
231 7
            return $response;
232 2
        } catch (Exception $e) {
233 2
            throw new TransmissionException($e->getCode(), $e);
234
        }
235
    }
236
237
    /**
238
     * @param SendGrid\Mail $email
239
     *
240
     * @return SendGrid\Response
241
     */
242 4
    protected function sendEmptyContent(SendGrid\Mail $email): SendGrid\Response
243
    {
244 4
        $email->addContent(new SendGrid\Content('text/plain', ''));
245
246 4
        return $this->send($email);
247
    }
248
249
    /**
250
     * @param SendGrid\Mail                   $email
251
     * @param Content\Contracts\SimpleContent $content
252
     *
253
     * @return SendGrid\Response
254
     */
255 4
    protected function sendSimpleContent(
256
        SendGrid\Mail $email,
257
        Content\Contracts\SimpleContent $content
258
    ): SendGrid\Response {
259 4
        if ($content->getHtml() !== null) {
260 2
            $email->addContent(new SendGrid\Content('text/html', $content->getHtml()->getBody()));
261 2
        } elseif ($content->getText() !== null) {
262 1
            $email->addContent(new SendGrid\Content('text/plain', $content->getText()->getBody()));
263
        } else {
264 1
            $email->addContent(new SendGrid\Content('text/plain', ''));
265
        }
266
267 4
        return $this->send($email);
268
    }
269
270
    /**
271
     * @param SendGrid\Mail                      $email
272
     * @param Content\Contracts\TemplatedContent $content
273
     *
274
     * @return SendGrid\Response
275
     */
276 1
    protected function sendTemplatedContent(
277
        SendGrid\Mail $email,
278
        Content\Contracts\TemplatedContent $content
279
    ): SendGrid\Response {
280 1
        foreach ($content->getTemplateData() as $key => $value) {
281 1
            $email->personalization[0]->addSubstitution($key, $value);
282
        }
283
284 1
        $email->setTemplateId($content->getTemplateId());
285
286 1
        return $this->send($email);
287
    }
288
289
    /**
290
     * @param Email $email
291
     *
292
     * @return SendGrid\Attachment[]
293
     */
294 9
    protected function buildAttachments(Email $email): array
295
    {
296 9
        $attachments = [];
297
298 9
        foreach ($email->getAttachments() as $attachment) {
299 1
            $sendGridAttachment = new SendGrid\Attachment();
300 1
            $sendGridAttachment->setFilename($attachment->getName());
301 1
            $sendGridAttachment->setContent($attachment->getBase64Content());
302
303 1
            $attachments[] = $sendGridAttachment;
304
        }
305
306 9
        foreach ($email->getEmbedded() as $attachment) {
307 1
            $sendGridAttachment = new SendGrid\Attachment();
308 1
            $sendGridAttachment->setFilename($attachment->getName());
309 1
            $sendGridAttachment->setContent($attachment->getBase64Content());
310 1
            $sendGridAttachment->setContentID($attachment->getContentId());
311 1
            $sendGridAttachment->setDisposition('inline');
312
313 1
            $attachments[] = $sendGridAttachment;
314
        }
315
316 9
        return $attachments;
317
    }
318
}
319