Completed
Pull Request — master (#3)
by Chris
02:06
created

SparkPostCourier::createAddress()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 2
dl 0
loc 6
ccs 4
cts 4
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Courier\Sparkpost;
6
7
use Courier\ConfirmingCourier;
8
use Courier\Exceptions\TransmissionException;
9
use Courier\Exceptions\UnsupportedContentException;
10
use Courier\SavesReceipts;
11
use PhpEmail\Address;
12
use PhpEmail\Content;
13
use PhpEmail\Email;
14
use Psr\Log\LoggerInterface;
15
use Psr\Log\NullLogger;
16
use SparkPost\SparkPost;
17
use SparkPost\SparkPostException;
18
use SparkPost\SparkPostResponse;
19
20
/**
21
 * A courier implementation using SparkPost as the third-party provider. This library uses the web API and the
22
 * php-sparkpost library to send transmissions.
23
 *
24
 * An important note is that while the SparkPost API does not support sending attachments on templated transmissions,
25
 * this API simulates the feature by creating an inline template based on the defined template using the API. In this
26
 * case, all template variables will be sent as expected, but tracking/reporting may not work as expected within
27
 * SparkPost.
28
 */
29
class SparkPostCourier implements ConfirmingCourier
30
{
31
    use SavesReceipts;
32
33
    const RECIPIENTS        = 'recipients';
34
    const CC                = 'cc';
35
    const BCC               = 'bcc';
36
    const REPLY_TO          = 'reply_to';
37
    const SUBSTITUTION_DATA = 'substitution_data';
38
39
    const CONTENT       = 'content';
40
    const FROM          = 'from';
41
    const SUBJECT       = 'subject';
42
    const HTML          = 'html';
43
    const TEXT          = 'text';
44
    const INLINE_IMAGES = 'inline_images';
45
    const ATTACHMENTS   = 'attachments';
46
    const TEMPLATE_ID   = 'template_id';
47
48
    const HEADERS   = 'headers';
49
    const CC_HEADER = 'CC';
50
51
    const ADDRESS       = 'address';
52
    const CONTACT_NAME  = 'name';
53
    const CONTACT_EMAIL = 'email';
54
    const HEADER_TO     = 'header_to';
55
56
    const ATTACHMENT_NAME = 'name';
57
    const ATTACHMENT_TYPE = 'type';
58
    const ATTACHMENT_DATA = 'data';
59
60
    /**
61
     * @var SparkPost
62
     */
63
    private $sparkPost;
64
65
    /**
66
     * @var SparkPostTemplates
67
     */
68
    private $templates;
69
70
    /**
71
     * @var LoggerInterface
72
     */
73
    private $logger;
74
75
76
77
    /**
78
     * @param SparkPost          $sparkPost
79
     * @param SparkPostTemplates $templates
80
     * @param LoggerInterface    $logger
81
     */
82 14
    public function __construct(SparkPost $sparkPost, SparkPostTemplates $templates = null, LoggerInterface $logger = null)
83
    {
84 14
        $this->sparkPost = $sparkPost;
85 14
        $this->templates = $templates ?: new SparkPostTemplates($sparkPost, $logger);
86 14
        $this->logger    = $logger ?: new NullLogger();
87
    }
88
89 14
    public function deliver(Email $email): void
90
    {
91 14
        if (!$this->supportsContent($email->getContent())) {
92 1
            throw new UnsupportedContentException($email->getContent());
93
        }
94
95 13
        $mail = $this->prepareEmail($email);
96 13
        $mail = $this->prepareContent($email, $mail);
97
98 11
        $response = $this->send($mail);
99
100 10
        $this->saveReceipt($email, $response->getBody()['results']['id']);
101
    }
102
103
    /**
104
     * @param array $mail
105
     *
106
     * @return SparkPostResponse
107
     */
108 11
    protected function send(array $mail): SparkPostResponse
109
    {
110 11
        $promise = $this->sparkPost->transmissions->post($mail);
111
112
        try {
113 11
            return $promise->wait();
114 1
        } catch (SparkPostException $e) {
115 1
            $this->logger->error(
116 1
                'Received status {code} from SparkPost with body: {body}',
117
                [
118 1
                    'code' => $e->getCode(),
119 1
                    'body' => $e->getBody(),
120
                ]
121
            );
122
123 1
            throw new TransmissionException($e->getCode(), $e);
124
        }
125
    }
126
127
    /**
128
     * @return array
129
     */
130 14
    protected function supportedContent(): array
131
    {
132
        return [
133 14
            Content\EmptyContent::class,
134
            Content\Contracts\SimpleContent::class,
135
            Content\Contracts\TemplatedContent::class,
136
        ];
137
    }
138
139
    /**
140
     * Determine if the content is supported by this courier.
141
     *
142
     * @param Content $content
143
     *
144
     * @return bool
145
     */
146 14
    protected function supportsContent(Content $content): bool
147
    {
148 14
        foreach ($this->supportedContent() as $contentType) {
149 14
            if ($content instanceof $contentType) {
150 14
                return true;
151
            }
152
        }
153
154 1
        return false;
155
    }
156
157
    /**
158
     * @param Email $email
159
     *
160
     * @return array
161
     */
162 13
    protected function prepareEmail(Email $email): array
163
    {
164 13
        $message  = [];
165 13
        $headerTo = $this->buildHeaderTo($email);
166
167 13
        $message[self::RECIPIENTS] = [];
168
169 13
        foreach ($email->getToRecipients() as $recipient) {
170 13
            $message[self::RECIPIENTS][] = $this->createAddress($recipient, $headerTo);
171
        }
172
173 13
        foreach ($email->getCcRecipients() as $recipient) {
174 5
            $message[self::RECIPIENTS][] = $this->createAddress($recipient, $headerTo);
175
        }
176
177 13
        foreach ($email->getBccRecipients() as $recipient) {
178 3
            $message[self::RECIPIENTS][] = $this->createAddress($recipient, $headerTo);
179
        }
180
181 13
        return $message;
182
    }
183
184
    /**
185
     * @param Email $email
186
     * @param array $message
187
     *
188
     * @return array
189
     */
190 13
    protected function prepareContent(Email $email, array $message): array
191
    {
192
        switch (true) {
193 13
            case $email->getContent() instanceof Content\Contracts\TemplatedContent:
194 8
                $message[self::CONTENT]           = $this->buildTemplateContent($email);
195 6
                $message[self::SUBSTITUTION_DATA] = $this->buildTemplateData($email);
196
197 6
                break;
198
199 5
            case $email->getContent() instanceof Content\EmptyContent:
200 2
                $email->setContent(new Content\SimpleContent(
201 2
                    new Content\SimpleContent\Message(''),
202 2
                    new Content\SimpleContent\Message('')
203
                ));
204
205 2
                $message[self::CONTENT] = $this->buildSimpleContent($email);
206
207 2
                break;
208
209 3
            case $email->getContent() instanceof Content\SimpleContent:
210 3
                $message[self::CONTENT] = $this->buildSimpleContent($email);
211
        }
212
213 11
        return $message;
214
    }
215
216
    /**
217
     * Attempt to create template data using the from, subject and reply to, which SparkPost considers to be
218
     * part of the templates substitutable content.
219
     *
220
     * @param Email $email
221
     *
222
     * @return array
223
     */
224 6
    protected function buildTemplateData(Email $email): array
225
    {
226
        /** @var Content\TemplatedContent $emailContent */
227 6
        $emailContent = $email->getContent();
228 6
        $templateData = $emailContent->getTemplateData();
229
230 6
        if ($email->getReplyTos()) {
231 4
            $replyTos = $email->getReplyTos();
232 4
            $first    = reset($replyTos);
233
234 4
            if (!array_key_exists('replyTo', $templateData)) {
235 4
                $templateData['replyTo'] = $first->toRfc2822();
236
            }
237
        }
238
239 6
        if (!array_key_exists('fromName', $templateData)) {
240 6
            $templateData['fromName'] = $email->getFrom()->getName();
241
        }
242
243 6
        if (!array_key_exists('fromEmail', $templateData)) {
244 6
            $templateData['fromEmail'] = explode('@', $email->getFrom()->getEmail())[0];
245
        }
246
247 6
        if (!array_key_exists('fromDomain', $templateData)) {
248 6
            $templateData['fromDomain'] = explode('@', $email->getFrom()->getEmail())[1];
249
        }
250
251 6
        if (!array_key_exists('subject', $templateData)) {
252 6
            $templateData['subject'] = $email->getSubject();
253
        }
254
255
        // @TODO Remove this variable once SparkPost CC headers work properly for templates
256 6
        if (!array_key_exists('ccHeader', $templateData)) {
257 6
            if ($header = $this->buildCcHeader($email)) {
258 3
                $templateData['ccHeader'] = $header;
259
            }
260
        }
261
262 6
        return $templateData;
263
    }
264
265
    /**
266
     * @param Email $email
267
     *
268
     * @return array
269
     */
270 8
    protected function buildTemplateContent(Email $email): array
271
    {
272
        // SparkPost does not currently support templated emails with attachments, so it must be converted to a
273
        // SimpleContent message instead.
274 8
        if ($email->getAttachments()) {
275 6
            return $this->buildSimpleContent($this->templates->convertTemplatedEmail($email));
276
        }
277
278
        $content = [
279 2
            self::TEMPLATE_ID => $email->getContent()->getTemplateId(),
0 ignored issues
show
Bug introduced by
The method getTemplateId() does not exist on PhpEmail\Content. It seems like you code against a sub-type of PhpEmail\Content such as PhpEmail\Content\Contracts\TemplatedContent. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

279
            self::TEMPLATE_ID => $email->getContent()->/** @scrutinizer ignore-call */ getTemplateId(),
Loading history...
280
        ];
281
282 2
        if ($headers = $this->getContentHeaders($email)) {
283 1
            $content[self::HEADERS] = $headers;
284
        }
285
286 2
        return $content;
287
    }
288
289
    /**
290
     * @param Email $email
291
     *
292
     * @return array
293
     */
294 9
    protected function buildSimpleContent(Email $email): array
295
    {
296 9
        $replyTo = null;
297 9
        if (!empty($email->getReplyTos())) {
298
            // SparkPost only supports a single reply-to
299 5
            $replyTos = $email->getReplyTos();
300 5
            $first    = reset($replyTos);
301
302 5
            $replyTo = $first->toRfc2822();
303
        }
304
305
        /** @var Content\Contracts\SimpleContent $emailContent */
306 9
        $emailContent = $email->getContent();
307
308
        $content = [
309 9
            self::FROM          => [
310 9
                self::CONTACT_NAME  => $email->getFrom()->getName(),
311 9
                self::CONTACT_EMAIL => $email->getFrom()->getEmail(),
312
            ],
313 9
            self::SUBJECT       => $email->getSubject(),
314 9
            self::HTML          => $emailContent->getHtml() !== null ? $emailContent->getHtml()->getBody() : null,
315 9
            self::TEXT          => $emailContent->getText() !== null ? $emailContent->getText()->getBody() : null,
316 9
            self::ATTACHMENTS   => $this->buildAttachments($email),
317 9
            self::INLINE_IMAGES => $this->buildInlineAttachments($email),
318 9
            self::REPLY_TO      => $replyTo,
319
        ];
320
321 9
        if ($headers = $this->getContentHeaders($email)) {
322 6
            $content[self::HEADERS] = $headers;
323
        }
324
325 9
        return $content;
326
    }
327
328 11
    protected function getContentHeaders(Email $email): array
329
    {
330 11
        $headers = [];
331
332 11
        foreach ($email->getHeaders() as $header) {
333 6
            $headers[$header->getField()] = $header->getValue();
334
        }
335
336 11
        if ($ccHeader = $this->buildCcHeader($email)) {
337 5
            $headers[self::CC_HEADER] = $ccHeader;
338
        } else {
339
            // If this was set on a template in SparkPost, we will remove it, because there are no
340
            // CCs defined on the email itself.
341 6
            unset($headers[self::CC_HEADER]);
342
        }
343
344 11
        return $headers;
345
    }
346
347
    /**
348
     * @param Email $email
349
     *
350
     * @return array
351
     */
352 9
    private function buildAttachments(Email $email): array
353
    {
354 9
        $attachments = [];
355
356 9
        foreach ($email->getAttachments() as $attachment) {
357 6
            $attachments[] = [
358 6
                self::ATTACHMENT_NAME => $attachment->getName(),
359 6
                self::ATTACHMENT_TYPE => $attachment->getRfc2822ContentType(),
360 6
                self::ATTACHMENT_DATA => $attachment->getBase64Content(),
361
            ];
362
        }
363
364 9
        return $attachments;
365
    }
366
367
    /**
368
     * @param Email $email
369
     *
370
     * @return array
371
     */
372 9
    private function buildInlineAttachments(Email $email): array
373
    {
374 9
        $inlineAttachments = [];
375
376 9
        foreach ($email->getEmbedded() as $embedded) {
377 4
            $inlineAttachments[] = [
378 4
                self::ATTACHMENT_NAME => $embedded->getContentId(),
379 4
                self::ATTACHMENT_TYPE => $embedded->getRfc2822ContentType(),
380 4
                self::ATTACHMENT_DATA => $embedded->getBase64Content(),
381
            ];
382
        }
383
384 9
        return $inlineAttachments;
385
    }
386
387
    /**
388
     * @param Address $address
389
     * @param string  $headerTo
390
     *
391
     * @return array
392
     */
393 13
    private function createAddress(Address $address, string $headerTo): array
394
    {
395
        return [
396 13
            self::ADDRESS => [
397 13
                self::CONTACT_EMAIL => $address->getEmail(),
398 13
                self::HEADER_TO     => $headerTo,
399
            ],
400
        ];
401
    }
402
403
    /**
404
     * Build a string representing the header_to field of this email.
405
     *
406
     * @param Email $email
407
     *
408
     * @return string
409
     */
410
    private function buildHeaderTo(Email $email): string
411
    {
412 13
        return implode(',', array_map(function (Address $address) {
413 13
            return $address->toRfc2822();
414 13
        }, $email->getToRecipients()));
415
    }
416
417
    /**
418
     * Build a string representing the CC header for this email.
419
     *
420
     * @param Email $email
421
     *
422
     * @return string
423
     */
424
    private function buildCcHeader(Email $email): string
425
    {
426 11
        return implode(',', array_map(function (Address $address) {
427 5
            return $address->toRfc2822();
428 11
        }, $email->getCcRecipients()));
429
    }
430
}
431