Completed
Pull Request — master (#4)
by Chris
02:14
created

SendGridCourier::sendEmptyContent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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