Passed
Push — master ( 648877...0b09eb )
by Thomas
12:23
created

SparkPostApiClient::getSuppression()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 1
b 0
f 1
1
<?php
2
3
namespace LeKoala\SparkPost\Api;
4
5
use Exception;
6
use InvalidArgumentException;
7
use DateTime;
8
use LeKoala\SparkPost\EmailUtils;
9
10
/**
11
 * A really simple SparkPost api client
12
 *
13
 * @link https://developers.sparkpost.com/api/
14
 * @author LeKoala <[email protected]>
15
 */
16
class SparkPostApiClient
17
{
18
19
    // CLIENT SETTINGS
20
    const CLIENT_VERSION = '0.2';
21
    const API_ENDPOINT = 'https://api.sparkpost.com/api/v1';
22
    const API_ENDPOINT_EU = 'https://api.eu.sparkpost.com/api/v1';
23
    const METHOD_GET = "GET";
24
    const METHOD_POST = "POST";
25
    const METHOD_PUT = "PUT";
26
    const METHOD_DELETE = "DELETE";
27
    const DATETIME_FORMAT = 'Y-m-d\TH:i';
28
    // SPARKPOST TYPES
29
    const TYPE_MESSAGE = 'message_event'; // Bounce, Delivery, Injection, SMS Status, Spam Complaint, Out of Band, Policy Rejection, Delay
30
    const TYPE_ENGAGEMENT = 'track_event'; // Click, Open
31
    const TYPE_GENERATION = 'gen_event'; // Generation Failure, Generation Rejection
32
    const TYPE_UNSUBSCRIBE = 'unsubscribe_event'; // List Unsubscribe, Link Unsubscribe
33
    const TYPE_RELAY = 'relay_event'; // Relay Injection, Relay Rejection, Relay Delivery, Relay Temporary Failure, Relay Permanent Failure
34
    // SPARKPOST EVENTS
35
    const EVENT_DELIVERY = 'delivery';
36
    const EVENT_BOUNCE = 'bounce';
37
    const EVENT_INJECTION = 'injection';
38
    const EVENT_SMS_STATUS = 'sms_status';
39
    const EVENT_SPAM_COMPLAINT = 'spam_complaint';
40
    const EVENT_OUT_OF_BAND = 'out_of_band';
41
    const EVENT_POLICY_REJECTION = 'policy_rejection';
42
    const EVENT_DELAY = 'delay';
43
    const EVENT_OPEN = 'open';
44
    const EVENT_CLICK = 'click';
45
    const EVENT_GEN_FAILURE = 'generation_failure';
46
    const EVENT_GEN_REJECTION = 'generation_rejection';
47
    const EVENT_LIST_UNSUB = 'list_unsubscribe';
48
    const EVENT_LINK_UNSUB = 'link_unsubscribe';
49
    const EVENT_RELAY_INJECTION = 'relay_injection';
50
    const EVENT_RELAY_REJECTION = 'relay_rejection';
51
    const EVENT_RELAY_DELIVERY = 'relay_delivery';
52
    const EVENT_RELAY_TEMPFAIL = 'relay_tempfail';
53
    const EVENT_RELAY_PERMFAIL = 'relay_permfail';
54
55
    /**
56
     * Your api key
57
     *
58
     * @var ?string
59
     */
60
    protected $key;
61
62
    /**
63
     * Is eu endpoint ?
64
     *
65
     * @var boolean
66
     */
67
    protected $euEndpoint = false;
68
69
    /**
70
     * Curl verbose log
71
     *
72
     * @var string
73
     */
74
    protected $verboseLog = '';
75
76
    /**
77
     * A callback to log results
78
     *
79
     * @var callable
80
     */
81
    protected $logger;
82
83
    /**
84
     * Results from the api
85
     *
86
     * @var array<mixed>
87
     */
88
    protected $results = [];
89
90
    /**
91
     * The ID of the subaccount to use
92
     *
93
     * @var int
94
     */
95
    protected $subaccount;
96
97
    /**
98
     * Client options
99
     *
100
     * @var array<mixed>
101
     */
102
    protected $curlOpts = [];
103
104
    /**
105
     * Create a new instance of the SparkPostApiClient
106
     *
107
     * @param string $key Specify the string, or it will read env SPARKPOST_API_KEY or constant SPARKPOST_API_KEY
108
     * @param int $subaccount Specify a subaccount to limit data sent by the API
109
     * @param array<mixed> $curlOpts Additionnal options to configure the curl client
110
     */
111
    public function __construct($key = null, $subaccount = null, $curlOpts = [])
112
    {
113
        if ($key) {
114
            $this->key = $key;
115
        } else {
116
            $envkey = getenv('SPARKPOST_API_KEY');
117
            if ($envkey) {
118
                $this->key = $envkey;
119
            }
120
        }
121
        if (getenv('SPARKPOST_EU')) {
122
            $this->euEndpoint = boolval(getenv('SPARKPOST_EU'));
123
        } elseif (defined('SPARKPOST_EU')) {
124
            $this->euEndpoint = true;
125
        }
126
        $this->subaccount = $subaccount;
127
        $this->curlOpts = array_merge($this->getDefaultCurlOptions(), $curlOpts);
128
    }
129
130
    /**
131
     * Get default options
132
     *
133
     * @return array<mixed>
134
     */
135
    public function getDefaultCurlOptions()
136
    {
137
        return [
138
            'connect_timeout' => 10,
139
            'timeout' => 10,
140
            'verbose' => false,
141
        ];
142
    }
143
144
    /**
145
     * Get an option
146
     *
147
     * @param string $name
148
     * @return mixed
149
     */
150
    public function getCurlOption($name)
151
    {
152
        if (!isset($this->curlOpts[$name])) {
153
            throw new InvalidArgumentException("$name is not a valid option. Valid options are : " .
154
                implode(', ', array_keys($this->curlOpts)));
155
        }
156
        return $this->curlOpts[$name];
157
    }
158
159
    /**
160
     * Set an option
161
     *
162
     * @param string $name
163
     * @param mixed $value
164
     * @return void
165
     */
166
    public function setCurlOption($name, $value)
167
    {
168
        $this->curlOpts[$name] = $value;
169
    }
170
171
    /**
172
     * Get the current api key
173
     *
174
     * @return string
175
     */
176
    public function getKey()
177
    {
178
        return $this->key;
179
    }
180
181
    /**
182
     * Set the current api key
183
     *
184
     * @param string $key
185
     * @return void
186
     */
187
    public function setKey($key)
188
    {
189
        $this->key = $key;
190
    }
191
192
    /**
193
     * Get the use of eu endpoint
194
     *
195
     * @return bool
196
     */
197
    public function getEuEndpoint()
198
    {
199
        return $this->euEndpoint;
200
    }
201
202
    /**
203
     * Set the use of eu endpoint
204
     *
205
     * @param bool $euEndpoint
206
     * @return void
207
     */
208
    public function setEuEndpoint($euEndpoint)
209
    {
210
        $this->euEndpoint = $euEndpoint;
211
    }
212
213
    /**
214
     * Get verbose log
215
     *
216
     * @return string
217
     */
218
    public function getVerboseLog()
219
    {
220
        return $this->verboseLog;
221
    }
222
223
    /**
224
     * Get the logger
225
     *
226
     * @return callable
227
     */
228
    public function getLogger()
229
    {
230
        return $this->logger;
231
    }
232
233
    /**
234
     * Set a logging method
235
     *
236
     * @param callable $logger
237
     * @return void
238
     */
239
    public function setLogger(callable $logger)
240
    {
241
        $this->logger = $logger;
242
    }
243
244
    /**
245
     * Get subaccount id
246
     *
247
     * @return int
248
     */
249
    public function getSubaccount()
250
    {
251
        return $this->subaccount;
252
    }
253
254
    /**
255
     * Set subaccount id
256
     *
257
     * @param int $subaccount
258
     * @return void
259
     */
260
    public function setSubaccount($subaccount)
261
    {
262
        $this->subaccount = $subaccount;
263
    }
264
265
    /**
266
     * Helper that handles dot notation
267
     *
268
     * @param array<mixed> $arr
269
     * @param string $path
270
     * @param string $val
271
     * @return mixed
272
     */
273
    protected function setMappedValue(array &$arr, $path, $val)
274
    {
275
        $loc = &$arr;
276
        foreach (explode('.', $path) as $step) {
277
            $loc = &$loc[$step];
278
        }
279
        return $loc = $val;
280
    }
281
282
    /**
283
     * Map data using a given mapping array
284
     *
285
     * @param array<mixed> $data
286
     * @param array<mixed> $map
287
     * @return array<mixed>
288
     */
289
    protected function mapData($data, $map)
290
    {
291
        $mappedData = [];
292
        foreach ($data as $k => $v) {
293
            $key = $k;
294
            if (isset($map[$k])) {
295
                $key = $map[$k];
296
            }
297
            $this->setMappedValue($mappedData, $key, $v);
298
        }
299
        return $mappedData;
300
    }
301
302
    /**
303
     * Create a transmission
304
     *
305
     * 'campaign'
306
     * 'metadata'
307
     * 'substitutionData'
308
     * 'description'
309
     * 'returnPath'
310
     * 'replyTo'
311
     * 'subject'
312
     * 'from'
313
     * 'html'
314
     * 'text'
315
     * 'attachments'
316
     * 'rfc822'
317
     * 'customHeaders'
318
     * 'recipients'
319
     * 'recipientList'
320
     * 'template'
321
     * 'trackOpens'
322
     * 'trackClicks'
323
     * 'startTime'
324
     * 'transactional'
325
     * 'sandbox'
326
     * 'useDraftTemplate'
327
     * 'inlineCss'
328
     *
329
     * @link https://developers.sparkpost.com/api/transmissions.html
330
     * @param array<mixed> $data
331
     * @return array<mixed> An array containing 3 keys: total_rejected_recipients, total_accepted_recipients, id
332
     */
333
    public function createTransmission($data)
334
    {
335
        // Use the same mapping as official sdk
336
        $mapping = [
337
            'campaign' => 'campaign_id',
338
            'metadata' => 'metadata',
339
            'substitutionData' => 'substitution_data',
340
            'description' => 'description',
341
            'returnPath' => 'return_path',
342
            'replyTo' => 'content.reply_to',
343
            'subject' => 'content.subject',
344
            'from' => 'content.from',
345
            'html' => 'content.html',
346
            'text' => 'content.text',
347
            'attachments' => 'content.attachments',
348
            'rfc822' => 'content.email_rfc822',
349
            'customHeaders' => 'content.headers',
350
            'recipients' => 'recipients',
351
            'recipientList' => 'recipients.list_id',
352
            'template' => 'content.template_id',
353
            'trackOpens' => 'options.open_tracking',
354
            'trackClicks' => 'options.click_tracking',
355
            'startTime' => 'options.start_time',
356
            'transactional' => 'options.transactional',
357
            'sandbox' => 'options.sandbox',
358
            'useDraftTemplate' => 'use_draft_template',
359
            'inlineCss' => 'options.inline_css',
360
        ];
361
362
        $data = $this->mapData($data, $mapping);
363
364
        return $this->makeRequest('transmissions', self::METHOD_POST, $data);
365
    }
366
367
    /**
368
     * Get the detail of a transmission
369
     *
370
     * @param string $id
371
     * @return array<mixed>
372
     */
373
    public function getTransmission($id)
374
    {
375
        return $this->makeRequest('transmissions/' . $id);
376
    }
377
378
    /**
379
     * Delete a transmission
380
     *
381
     * @param string $id
382
     * @return array<mixed>
383
     */
384
    public function deleteTransmission($id)
385
    {
386
        return $this->makeRequest('transmissions/' . $id, self::METHOD_DELETE);
387
    }
388
389
    /**
390
     * List tranmssions
391
     *
392
     * @param string $campaignId
393
     * @param string $templateId
394
     * @return array<mixed>
395
     */
396
    public function listTransmissions($campaignId = null, $templateId = null)
397
    {
398
        $params = [];
399
        if ($campaignId !== null) {
400
            $params['campaign_id'] = $campaignId;
401
        }
402
        if ($templateId !== null) {
403
            $params['template_id'] = $templateId;
404
        }
405
        return $this->makeRequest('transmissions', self::METHOD_GET, $params);
406
    }
407
408
    /**
409
     * Search message events
410
     *
411
     * Use the following parameters (default is current timezone, 100 messages for the last 7 days)
412
     *
413
     * 'bounce_classes' : delimited list of bounce classification codes to search.
414
     * 'campaign_ids' : delimited list of campaign ID's to search (i.e. campaign_id used during creation of a transmission).
415
     * 'delimiter' : Specifies the delimiter for query parameter lists
416
     * 'events' : delimited list of event types to search.  Example: delivery, injection, bounce, delay, policy_rejection, out_of_band, open, click, ...
417
     * 'friendly_froms' : delimited list of friendly_froms to search.
418
     * 'from' : Datetime in format of YYYY-MM-DDTHH:MM.
419
     * 'message_ids' : delimited list of message ID's to search.
420
     * 'page' : The results page number to return. Used with per_page for paging through results
421
     * 'per_page' : Number of results to return per page. Must be between 1 and 10,000 (inclusive).
422
     * 'reason' : Bounce/failure/rejection reason that will be matched using a wildcard (e.g., %reason%)
423
     * 'recipients' : delimited list of recipients to search.
424
     * 'subaccounts' :  delimited list of subaccount ID’s to search..
425
     * 'template_ids' : delimited list of template ID's to search.
426
     * 'timezone' : Standard timezone identification string
427
     * 'to' : Datetime in format of YYYY-MM-DDTHH:MM
428
     * 'transmission_ids' : delimited list of transmission ID's to search (i.e. id generated during creation of a transmission).
429
     *
430
     * Result is an array that looks like this
431
     *
432
     * [customer_id] => 0000
433
     * [delv_method] => esmtp
434
     * [event_id] => 99997643157770993
435
     * [friendly_from] => [email protected]
436
     * [ip_address] => 12.34.56.78
437
     * [message_id] => abcd2fd71057477a0fa5
438
     * [msg_from] => [email protected]
439
     * [msg_size] => 1234
440
     * [num_retries] => 0
441
     * [queue_time] => 1234
442
     * [raw_rcpt_to] => [email protected]
443
     * [rcpt_meta] => Array
444
     * [rcpt_tags] => Array
445
     * [rcpt_to] => [email protected]
446
     * [routing_domain] => email.ext
447
     * [subaccount_id] => 0000
448
     * [subject] => my test subject
449
     * [tdate] => 2050-01-01T11:57:36.000Z
450
     * [template_id] => template_123456789
451
     * [template_version] => 0
452
     * [transactional] => 1
453
     * [transmission_id] => 12234554568854
454
     * [type] => delivery
455
     * [timestamp] =>  2050-01-01T11:57:36.000Z
456
     *
457
     * @deprecated
458
     * @param array<mixed> $params
459
     * @return array<mixed>
460
     */
461
    public function searchMessageEvents($params = [])
462
    {
463
        $defaultParams = [
464
            'timezone' => date_default_timezone_get(),
465
            'per_page' => 100,
466
            'from' => $this->createValidDatetime('-7 days'),
467
        ];
468
        $params = array_merge($defaultParams, $params);
469
470
        return $this->makeRequest('message-events', self::METHOD_GET, $params);
471
    }
472
473
    /**
474
     * Search for Message Events
475
     *
476
     * Parameters
477
     * - from string, default is 24 hours ago
478
     * - per_page number, default is 1000
479
     * - event_ids string
480
     * - events string, default is all event types
481
     * - recipients string
482
     * - recipient_domains string
483
     * - from_addresses string
484
     * - sending_domains string
485
     * - subjects string
486
     * - bounce_classes number
487
     * - reasons string
488
     * - campaigns string
489
     * - templates string
490
     * - sending_ips string
491
     * - ip_pools string
492
     * - subaccounts string
493
     * - messages string
494
     * - transmissions string
495
     * - mailbox_providers string
496
     * - mailbox_provider_regions string
497
     * - ab_tests string
498
     * - ab_test_versions number
499
     *
500
     * Result is an array of objects that looks like this
501
     *
502
     * "mailbox_provider" => "SomeProvider"
503
     * "template_version" => "0"
504
     * "friendly_from" => "[email protected]"
505
     * "subject" => "My email"
506
     * "ip_pool" => "default"
507
     * "sending_domain" => "testing.example.com"
508
     * "rcpt_tags" => []
509
     * "type" => "initial_open"
510
     * "mailbox_provider_region" => "Global"
511
     * "raw_rcpt_to" => "[email protected]"
512
     * "msg_from" => "[email protected]"
513
     * "geo_ip" => array:8 [▶]
514
     * "rcpt_to" => "[email protected]"
515
     * "subaccount_id" => 1
516
     * "transmission_id" => "1230984717797820762"
517
     * "user_agent" => "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0 (via ggpht.com GoogleImageProxy)"
518
     * "timestamp" => "2055-02-19T14:44:29.000Z"
519
     * "click_tracking" => true
520
     * "rcpt_meta" => []
521
     * "message_id" => "122da1ce2f606b5c07dc"
522
     * "ip_address" => "12.123.11.11"
523
     * "initial_pixel" => true
524
     * "recipient_domain" => "dest.com"
525
     * "event_id" => "5454130313444582480096"
526
     * "routing_domain" => "dest.com"
527
     * "sending_ip" => "99.99.99.99"
528
     * "template_id" => "template_693098471779782565"
529
     * "delv_method" => "esmtp"
530
     * "customer_id" => 99999
531
     * "open_tracking" => true
532
     * "injection_time" => "2055-02-19T14:43:45.000Z"
533
     * "transactional" => "1"
534
     * "msg_size" => "48613"
535
     *
536
     * @param array<mixed> $params
537
     * @return array<mixed>
538
     */
539
    public function searchEvents($params = [])
540
    {
541
        return $this->makeRequest('events/message', self::METHOD_GET, $params);
542
    }
543
544
    /**
545
     * Create a webhook by providing a webhooks object as the POST request body.
546
     * On creation, events will begin to be pushed to the target URL specified in the POST request body.
547
     *
548
     * {
549
     * "name": "Example webhook",
550
     * "target": "http://client.example.com/example-webhook",
551
     * "auth_type": "oauth2",
552
     * "auth_request_details": {
553
     * "url": "http://client.example.com/tokens",
554
     * "body": {
555
     *   "client_id": "CLIENT123",
556
     *   "client_secret": "9sdfj791d2bsbf",
557
     *   "grant_type": "client_credentials"
558
     * }
559
     * },
560
     * "auth_token": "",
561
     *   "events": [
562
     *   "delivery",
563
     *   "injection",
564
     *   "open",
565
     *   "click"
566
     * ]
567
     * }
568
     *
569
     * @param array<mixed> $params
570
     * @return array<mixed>
571
     */
572
    public function createWebhook($params = [])
573
    {
574
        return $this->makeRequest('webhooks', self::METHOD_POST, $params);
575
    }
576
577
    /**
578
     * A simpler call to the api
579
     *
580
     * @param string $name
581
     * @param string $target
582
     * @param array<mixed> $events
583
     * @param bool $auth Should we use basic auth ?
584
     * @param array<mixed> $credentials An array containing "username" and "password"
585
     * @return array<mixed>
586
     */
587
    public function createSimpleWebhook($name, $target, array $events = null, $auth = false, $credentials = null)
588
    {
589
        if ($events === null) {
590
            // Default to the most used events
591
            $events = [
592
                'delivery', 'injection', 'open', 'click', 'bounce', 'spam_complaint',
593
                'list_unsubscribe', 'link_unsubscribe'
594
            ];
595
        }
596
        $params = [
597
            'name' => $name,
598
            'target' => $target,
599
            'events' => $events,
600
        ];
601
        if ($auth) {
602
            if ($credentials === null) {
603
                $credentials = ['username' => "sparkpost", "password" => "sparkpost"];
604
            }
605
            $params['auth_type'] = 'basic';
606
            $params['auth_credentials'] = $credentials;
607
        }
608
        return $this->createWebhook($params);
609
    }
610
611
    /**
612
     * List all webhooks
613
     *
614
     * @param string $timezone
615
     * @return array<mixed>
616
     */
617
    public function listAllWebhooks($timezone = null)
618
    {
619
        $params = [];
620
        if ($timezone) {
621
            $params['timezone'] = $timezone;
622
        }
623
        return $this->makeRequest('webhooks', self::METHOD_GET, $params);
624
    }
625
626
    /**
627
     * Get a webhook
628
     *
629
     * @param string $id
630
     * @return array<mixed>
631
     */
632
    public function getWebhook($id)
633
    {
634
        return $this->makeRequest('webhooks/' . $id, self::METHOD_GET);
635
    }
636
637
    /**
638
     * Update a webhook
639
     *
640
     * @param string $id
641
     * @param array<mixed> $params
642
     * @return array<mixed>
643
     */
644
    public function updateWebhook($id, $params = [])
645
    {
646
        return $this->makeRequest('webhooks/' . $id, self::METHOD_PUT, $params);
647
    }
648
649
    /**
650
     * Delete a webhook
651
     *
652
     * @param string $id
653
     * @return array<mixed>
654
     */
655
    public function deleteWebhook($id)
656
    {
657
        return $this->makeRequest('webhooks/' . $id, self::METHOD_DELETE);
658
    }
659
660
    /**
661
     * Validate a webhook
662
     *
663
     * @param string $id
664
     * @return array<mixed>
665
     */
666
    public function validateWebhook($id)
667
    {
668
        return $this->makeRequest('webhooks/' . $id . '/validate', self::METHOD_POST, '{"msys": {}}');
669
    }
670
671
    /**
672
     * Retrieve status information regarding batches that have been generated
673
     * for the given webhook by specifying its id in the URI path. Status
674
     * information includes the successes of batches that previously failed to
675
     * reach the webhook's target URL and batches that are currently in a failed state.
676
     *
677
     * @param string $id
678
     * @param int $limit
679
     * @return array<mixed>
680
     */
681
    public function webhookBatchStatus($id, $limit = 1000)
0 ignored issues
show
Unused Code introduced by
The parameter $limit is not used and could be removed. ( Ignorable by Annotation )

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

681
    public function webhookBatchStatus($id, /** @scrutinizer ignore-unused */ $limit = 1000)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
682
    {
683
        return $this->makeRequest('webhooks/' . $id . '/batch-status', self::METHOD_GET, ['limit' => 1000]);
684
    }
685
686
    /**
687
     * List an example of the event data that will be posted by a Webhook for the specified events.
688
     *
689
     * @param string $events bounce, delivery...
690
     * @return array<mixed>
691
     */
692
    public function getSampleEvents($events = null)
693
    {
694
        $params = [];
695
        if ($events) {
696
            $params['events'] = $events;
697
        }
698
        return $this->makeRequest('webhooks/events/samples/', self::METHOD_GET, $params);
699
    }
700
701
    /**
702
     * Create a sending domain
703
     *
704
     * @param array<mixed> $params
705
     * @return array<mixed>
706
     */
707
    public function createSendingDomain($params = [])
708
    {
709
        return $this->makeRequest('sending-domains', self::METHOD_POST, $params);
710
    }
711
712
    /**
713
     * A simpler call to the api
714
     *
715
     * @param string $name
716
     * @return array<mixed>
717
     */
718
    public function createSimpleSendingDomain($name)
719
    {
720
        $params = [
721
            'domain' => $name,
722
        ];
723
        return $this->createSendingDomain($params);
724
    }
725
726
    /**
727
     * List all sending domains
728
     *
729
     * @return array<mixed>
730
     */
731
    public function listAllSendingDomains()
732
    {
733
        return $this->makeRequest('sending-domains', self::METHOD_GET);
734
    }
735
736
    /**
737
     * Get a sending domain
738
     *
739
     * @param string $id
740
     * @return array<mixed>
741
     */
742
    public function getSendingDomain($id)
743
    {
744
        return $this->makeRequest('sending-domains/' . $id, self::METHOD_GET);
745
    }
746
747
    /**
748
     * Verify a sending domain - This will ask SparkPost to check if SPF and DKIM are valid
749
     *
750
     * @param string $id
751
     * @return array<mixed>
752
     */
753
    public function verifySendingDomain($id)
754
    {
755
        return $this->makeRequest('sending-domains/' . $id . '/verify', self::METHOD_POST, [
756
            'dkim_verify' => true,
757
            'spf_verify' => true
758
        ]);
759
    }
760
761
    /**
762
     * Update a sending domain
763
     *
764
     * @param string $id
765
     * @param array<mixed> $params
766
     * @return array<mixed>
767
     */
768
    public function updateSendingDomain($id, $params = [])
769
    {
770
        return $this->makeRequest('sending-domains/' . $id, self::METHOD_PUT, $params);
771
    }
772
773
    /**
774
     * Delete a sending domain
775
     *
776
     * @param string $id
777
     * @return array<mixed>
778
     */
779
    public function deleteSendingDomain($id)
780
    {
781
        return $this->makeRequest('sending-domains/' . $id, self::METHOD_DELETE);
782
    }
783
784
    /**
785
     * Create an inbound domain
786
     *
787
     * @param string $domain
788
     * @return array<mixed>
789
     */
790
    public function createInboundDomain($domain)
791
    {
792
        return $this->makeRequest('inbound-domains', self::METHOD_POST, ['domain' => $domain]);
793
    }
794
795
    /**
796
     * List all inbound domains
797
     *
798
     * @return array<mixed>
799
     */
800
    public function listInboundDomains()
801
    {
802
        return $this->makeRequest('inbound-domains', self::METHOD_GET);
803
    }
804
805
    /**
806
     * Get details of an inbound domain
807
     *
808
     * @param string $domain
809
     * @return array<mixed>
810
     */
811
    public function getInboundDomain($domain)
812
    {
813
        return $this->makeRequest('inbound-domains/' . $domain, self::METHOD_GET);
814
    }
815
816
    /**
817
     * Delete an inbound domain
818
     *
819
     * @param string $domain
820
     * @return array<mixed>
821
     */
822
    public function deleteInboundDomain($domain)
823
    {
824
        return $this->makeRequest('inbound-domains/' . $domain, self::METHOD_DELETE);
825
    }
826
827
    /**
828
     * Create a relay webhook
829
     *
830
     *  "name": "Replies Webhook",
831
     *  "target": "https://webhooks.customer.example/replies",
832
     * "auth_token": "5ebe2294ecd0e0f08eab7690d2a6ee69",
833
     *  "match": {
834
     *  "protocol": "SMTP",
835
     * "domain": "email.example.com"
836
     * }
837
     *
838
     * @param array<mixed>|string $params
839
     * @return array<mixed>
840
     */
841
    public function createRelayWebhook($params)
842
    {
843
        return $this->makeRequest('relay-webhooks', self::METHOD_POST, $params);
844
    }
845
846
    /**
847
     * List all relay webhooks
848
     *
849
     * @return array<mixed>
850
     */
851
    public function listRelayWebhooks()
852
    {
853
        return $this->makeRequest('relay-webhooks', self::METHOD_GET);
854
    }
855
856
    /**
857
     * Get the details of a relay webhook
858
     *
859
     * @param int $id
860
     * @return array<mixed>
861
     */
862
    public function getRelayWebhook($id)
863
    {
864
        return $this->makeRequest('relay-webhooks/' . $id, self::METHOD_GET);
865
    }
866
867
    /**
868
     * Update a relay webhook
869
     *
870
     * @param int $id
871
     * @param array<mixed> $params
872
     * @return array<mixed>
873
     */
874
    public function updateRelayWebhook($id, $params)
875
    {
876
        return $this->makeRequest('relay-webhooks/' . $id, self::METHOD_PUT, $params);
877
    }
878
879
    /**
880
     * Delete a relay webhook
881
     *
882
     * @param int $id
883
     * @return array<mixed>
884
     */
885
    public function deleteRelayWebhook($id)
886
    {
887
        return $this->makeRequest('relay-webhooks/' . $id, self::METHOD_DELETE);
888
    }
889
890
    /**
891
     * @link https://developers.sparkpost.com/api/suppression-list/#suppression-list-get-retrieve-a-suppression
892
     * @param string $recipient
893
     * @return array<array{"recipient":string,"type":string,"source":string,"description":string,"created":string,"updated":string,"transactional":bool,"subaccount_id"?:int}>
894
     */
895
    public function getSuppression($recipient)
896
    {
897
        return $this->makeRequest('suppression-list/' . $recipient, self::METHOD_GET);
898
    }
899
900
    /**
901
     * @link https://developers.sparkpost.com/api/suppression-list/#suppression-list-put-create-or-update-a-suppression
902
     * @param string $recipient
903
     * @param bool $isTransactional
904
     * @param string $description
905
     * @return array<mixed>
906
     */
907
    public function createSuppression($recipient, $isTransactional = true, $description = '')
908
    {
909
        return $this->makeRequest('suppression-list/' . $recipient, self::METHOD_PUT, [
910
            'type' => $isTransactional ? 'transactional' : 'non_transactional',
911
            'description' => $description
912
        ]);
913
    }
914
915
    /**
916
     * @param string $recipient
917
     * @return array<mixed>
918
     */
919
    public function deleteSuppression($recipient)
920
    {
921
        return $this->makeRequest('suppression-list/' . $recipient, self::METHOD_DELETE);
922
    }
923
924
    /**
925
     * @link https://developers.sparkpost.com/api/suppression-list/#suppression-list-get-search-suppressions
926
     * @param array{'from'?:string,'to'?:string,'domain'?:string,'sources'?:string,'types'?:string,'description'?:string} $params
927
     * @return array<array{"recipient":string,"type":string,"source":string,"description":string,"created":string,"updated":string,"transactional":bool}>
928
     */
929
    public function searchSuppressions($params = [])
930
    {
931
        $defaultParams = [
932
            'per_page' => 10,
933
            'from' => $this->createValidDatetime('-30 days'),
934
        ];
935
        $params = array_merge($defaultParams, $params);
936
937
        return $this->makeRequest('suppression-list', self::METHOD_GET, $params);
938
    }
939
940
    /**
941
     * @link https://developers.sparkpost.com/api/suppression-list/#suppression-list-get-retrieve-summary
942
     * @return array{"spam_complaint":int,"list_unsubscribe":int,"bounce_rule":int,"unsubscribe_link":int,"manually_added":int,"compliance":int,"total":int}
943
     */
944
    public function suppressionSummary()
945
    {
946
        //@phpstan-ignore-next-line
947
        return $this->makeRequest('suppression-list/summary', self::METHOD_GET);
948
    }
949
950
    /**
951
     * Create a valid date for the API
952
     *
953
     * @param string|int $time
954
     * @param string $format
955
     * @return string Datetime in format of YYYY-MM-DDTHH:MM
956
     */
957
    public function createValidDatetime($time, $format = null)
958
    {
959
        if (!is_int($time)) {
960
            $time = strtotime($time);
961
        }
962
        if (!$time) {
963
            throw new Exception("Invalid time");
964
        }
965
        if (!$format) {
966
            $dt = new DateTime('@' . $time);
967
        } else {
968
            $dt = DateTime::createFromFormat((string)$format, (string)$time);
969
        }
970
        if (!$dt) {
0 ignored issues
show
introduced by
$dt is of type DateTime, thus it always evaluated to true.
Loading history...
971
            throw new Exception("Invalid datetime");
972
        }
973
        return $dt->format(self::DATETIME_FORMAT);
974
    }
975
976
    /**
977
     * Build an address object
978
     *
979
     * @param string $email
980
     * @param string $name
981
     * @param string $header_to
982
     * @return array<mixed>
983
     */
984
    public function buildAddress($email, $name = null, $header_to = null)
985
    {
986
        $address = [
987
            'email' => $email
988
        ];
989
        if ($name) {
990
            $address['name'] = $name;
991
        }
992
        if ($header_to) {
993
            $address['header_to'] = $header_to;
994
        }
995
        return $address;
996
    }
997
998
    /**
999
     * Build an address object from a RFC 822 email string
1000
     *
1001
     * @param string $string
1002
     * @param string $header_to
1003
     * @return array<mixed>
1004
     */
1005
    public function buildAddressFromString($string, $header_to = null)
1006
    {
1007
        $email = EmailUtils::get_email_from_rfc_email($string);
1008
        $name = EmailUtils::get_displayname_from_rfc_email($string);
1009
        return $this->buildAddress($email, $name, $header_to);
1010
    }
1011
1012
    /**
1013
     * Build a recipient
1014
     *
1015
     * @param string|array<mixed> $address
1016
     * @param array<mixed> $tags
1017
     * @param array<mixed> $metadata
1018
     * @param array<mixed> $substitution_data
1019
     * @return array<mixed>
1020
     * @throws Exception
1021
     */
1022
    public function buildRecipient($address, array $tags = null, array $metadata = null, array $substitution_data = null)
1023
    {
1024
        if (is_array($address)) {
1025
            if (empty($address['email'])) {
1026
                throw new Exception('Address must contain an email');
1027
            }
1028
        }
1029
        $recipient = [
1030
            'address' => $address
1031
        ];
1032
1033
        if (!empty($tags)) {
1034
            $recipient['tags'] = $tags;
1035
        }
1036
        if (!empty($metadata)) {
1037
            $recipient['metadata'] = $metadata;
1038
        }
1039
        if (!empty($tags)) {
1040
            $recipient['substitution_data'] = $substitution_data;
1041
        }
1042
1043
        return $recipient;
1044
    }
1045
1046
    /**
1047
     * Make a request to the api using curl
1048
     *
1049
     * @param string $endpoint
1050
     * @param string $action
1051
     * @param array<mixed>|string $data
1052
     * @return array<mixed>
1053
     * @throws Exception
1054
     */
1055
    protected function makeRequest($endpoint, $action = null, $data = null)
1056
    {
1057
        if (!$this->key) {
1058
            throw new Exception('You must set an API key before making requests');
1059
        }
1060
1061
        $ch = curl_init();
1062
1063
        if ($action === null) {
1064
            $action = self::METHOD_GET;
1065
        } else {
1066
            $action = strtoupper($action);
1067
        }
1068
1069
        if (is_array($data) && !empty($data)) {
1070
            if ($action === self::METHOD_GET) {
1071
                $endpoint .= '?' . http_build_query($data);
1072
            }
1073
            if ($action === self::METHOD_POST) {
1074
                $data = json_encode($data);
1075
            }
1076
        }
1077
1078
        $header = [];
1079
        $header[] = 'Content-Type: application/json';
1080
        $header[] = 'Authorization: ' . $this->key;
1081
        if ($this->subaccount) {
1082
            $header[] = 'X-MSYS-SUBACCOUNT: ' . $this->subaccount;
1083
        }
1084
1085
        curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
1086
        curl_setopt($ch, CURLOPT_USERAGENT, 'SparkPostApiClient v' . self::CLIENT_VERSION);
1087
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
1088
        if ($this->euEndpoint) {
1089
            curl_setopt($ch, CURLOPT_URL, self::API_ENDPOINT_EU . '/' . $endpoint);
1090
        } else {
1091
            curl_setopt($ch, CURLOPT_URL, self::API_ENDPOINT . '/' . $endpoint);
1092
        }
1093
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, (int)$this->getCurlOption('connect_timeout'));
1094
        curl_setopt($ch, CURLOPT_TIMEOUT, (int)$this->getCurlOption('timeout'));
1095
1096
        // Collect verbose data in a stream
1097
        if ($this->getCurlOption('verbose')) {
1098
            curl_setopt($ch, CURLOPT_VERBOSE, true);
1099
            $verbose = fopen('php://temp', 'w+');
1100
            if ($verbose === false) {
1101
                throw new Exception("Failed to open stream");
1102
            }
1103
            curl_setopt($ch, CURLOPT_STDERR, $verbose);
1104
        }
1105
1106
        // This fixes ca cert issues if server is not configured properly
1107
        $cainfo = ini_get('curl.cainfo');
1108
        if ($cainfo !== false) {
1109
            if (strlen($cainfo) === 0) {
1110
                curl_setopt($ch, CURLOPT_CAINFO, \Composer\CaBundle\CaBundle::getBundledCaBundlePath());
1111
            }
1112
        }
1113
1114
        switch ($action) {
1115
            case self::METHOD_POST:
1116
                curl_setopt($ch, CURLOPT_POST, true);
1117
                curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
1118
                break;
1119
            case self::METHOD_DELETE:
1120
                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE");
1121
                break;
1122
        }
1123
1124
        $result = curl_exec($ch);
1125
1126
        if (!$result) {
1127
            throw new Exception('Error: "' . curl_error($ch) . '" - Code: ' . curl_errno($ch));
1128
        }
1129
        if (is_bool($result)) {
1130
            throw new Exception("CURLOPT_RETURNTRANSFER was not set");
1131
        }
1132
1133
        if ($this->getCurlOption('verbose')) {
1134
            rewind($verbose);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $verbose does not seem to be defined for all execution paths leading up to this point.
Loading history...
1135
            $this->verboseLog .= stream_get_contents($verbose);
1136
        }
1137
1138
        curl_close($ch);
1139
1140
        // In some cases, SparkPost api returns this strange empty result
1141
        if ($result == '{ }') {
1142
            $decodedResult = ['results' => null];
1143
        } else {
1144
            $decodedResult = json_decode($result, true);
1145
            if (!$decodedResult) {
1146
                throw new Exception("Failed to decode $result : " . json_last_error_msg());
1147
            }
1148
        }
1149
1150
        $this->results[] = $decodedResult;
1151
1152
        if (isset($decodedResult['errors'])) {
1153
            $errors = array_map(function ($item) use ($data) {
1154
                $message = $item['message'];
1155
                // Prepend code to message
1156
                if (isset($item['code'])) {
1157
                    $message = $item['code'] . ' - ' . $message;
1158
1159
                    // For invalid domains, append domain name to make error more useful
1160
                    if ($item['code'] == 7001) {
1161
                        $from = '';
1162
                        if (!is_array($data) && is_string($data)) {
1163
                            $data = json_decode($data, true);
1164
                        }
1165
                        if (isset($data['content']['from'])) {
1166
                            $from = $data['content']['from'];
1167
                        }
1168
                        if ($from && is_string($from)) {
1169
                            $fromat = strrchr($from, "@");
1170
                            if ($fromat) {
1171
                                $domain = substr($fromat, 1);
1172
                                $message .= ' (' . $domain . ')';
1173
                            }
1174
                        }
1175
                    }
1176
1177
                    // For invalid recipients, append recipients
1178
                    if ($item['code'] == 5002) {
1179
                        if (isset($data['recipients'])) {
1180
                            if (empty($data['recipients'])) {
1181
                                $message .= ' (empty recipients list)';
1182
                            } else {
1183
                                $addresses = [];
1184
                                if (is_array($data['recipients'])) {
1185
                                    foreach ($data['recipients'] as $recipient) {
1186
                                        $addresses[] = json_encode($recipient['address']);
1187
                                    }
1188
                                }
1189
                                $message .= ' (' . implode(',', $addresses) . ')';
1190
                            }
1191
                        } else {
1192
                            $message .= ' (no recipients defined)';
1193
                        }
1194
                    }
1195
                }
1196
                if (isset($item['description'])) {
1197
                    $message .= ': ' . $item['description'];
1198
                }
1199
                return $message;
1200
            }, $decodedResult['errors']);
1201
            throw new Exception("The API returned the following error(s) : " . implode("; ", $errors));
1202
        }
1203
1204
        return $decodedResult['results'];
1205
    }
1206
1207
    /**
1208
     * Get all results from the api
1209
     *
1210
     * @return array<mixed>
1211
     */
1212
    public function getResults()
1213
    {
1214
        return $this->results;
1215
    }
1216
1217
    /**
1218
     * Get last result
1219
     *
1220
     * @return array<mixed>
1221
     */
1222
    public function getLastResult()
1223
    {
1224
        return end($this->results);
1225
    }
1226
}
1227