Passed
Push — master ( 505a31...09a556 )
by Chris
03:27
created

SendGridCourier::buildAttachments()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 3

Importance

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