SparkPostHelper   F
last analyzed

Complexity

Total Complexity 89

Size/Duplication

Total Lines 524
Duplicated Lines 0 %

Importance

Changes 12
Bugs 2 Features 2
Metric Value
eloc 178
c 12
b 2
f 2
dl 0
loc 524
rs 2
wmc 89

32 Methods

Rating   Name   Duplication   Size   Complexity  
A isEmailSuppressed() 0 9 2
A getClient() 0 21 6
C resolveDefaultFromEmail() 0 33 12
A getMasterClient() 0 8 2
A getEnvForceSender() 0 3 1
A getEnvSendingDisabled() 0 3 1
A getApiKey() 0 3 1
A getMailer() 0 3 1
A getLogFolder() 0 7 2
A getEnvEnableLogging() 0 3 1
A getEnvSubaccountId() 0 3 1
A forceAdminEmailOverride() 0 3 1
A getEnvApiKey() 0 3 1
A getWebhookPassword() 0 3 1
A hasEnvSendingDisabled() 0 3 1
A getSubaccountId() 0 3 1
A resolveDefaultFromEmailType() 0 11 3
A removeSuppression() 0 3 1
A getTransportFromMailer() 0 6 1
A registerTransport() 0 14 2
C init() 0 40 11
A getWebhookUsername() 0 3 1
A getEnvMasterApiKey() 0 3 1
B isEmailDomainReady() 0 29 8
A isAdminEmail() 0 9 3
A createDefaultEmail() 0 10 2
A getLoggingEnabled() 0 6 2
A getSendingEnabled() 0 6 2
A isDefaultEmail() 0 4 1
B resolveDefaultToEmail() 0 22 8
A getSenderFromSiteConfig() 0 15 5
A addMetaData() 0 17 3

How to fix   Complexity   

Complex Class

Complex classes like SparkPostHelper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SparkPostHelper, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace LeKoala\SparkPost;
4
5
use Exception;
6
use ReflectionObject;
7
use SilverStripe\Control\Director;
8
use SilverStripe\Core\Environment;
9
use SilverStripe\Core\Config\Config;
10
use Symfony\Component\Mailer\Mailer;
11
use SilverStripe\Control\Email\Email;
12
use SilverStripe\SiteConfig\SiteConfig;
13
use SilverStripe\Core\Injector\Injector;
14
use SilverStripe\Core\Config\Configurable;
15
use LeKoala\SparkPost\Api\SparkPostApiClient;
16
use Symfony\Component\Mailer\MailerInterface;
17
use SilverStripe\Core\Injector\InjectorNotFoundException;
18
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
19
20
/**
21
 * This configurable class helps decoupling the api client from SilverStripe
22
 */
23
class SparkPostHelper
24
{
25
    use Configurable;
26
27
    const FROM_SITECONFIG = "SiteConfig";
28
    const FROM_ADMIN = "Admin";
29
    const FROM_DEFAULT = "Default";
30
31
    /**
32
     * Client instance
33
     *
34
     * @var ?\LeKoala\SparkPost\Api\SparkPostApiClient
35
     */
36
    protected static $client;
37
38
    /**
39
     * Get the mailer instance
40
     *
41
     * @return MailerInterface
42
     */
43
    public static function getMailer()
44
    {
45
        return Injector::inst()->get(MailerInterface::class);
46
    }
47
48
    /**
49
     * @param MailerInterface $mailer
50
     * @return \Symfony\Component\Mailer\Transport\AbstractTransport|SparkPostApiTransport
51
     */
52
    public static function getTransportFromMailer($mailer)
53
    {
54
        $r = new ReflectionObject($mailer);
55
        $p = $r->getProperty('transport');
56
        $p->setAccessible(true);
57
        return $p->getValue($mailer);
58
    }
59
60
    /**
61
     * @return string
62
     */
63
    public static function getApiKey()
64
    {
65
        return self::config()->api_key;
66
    }
67
68
    /**
69
     * Get the api client instance
70
     * @return SparkPostApiClient
71
     * @throws Exception
72
     */
73
    public static function getClient()
74
    {
75
        if (!self::$client) {
76
            $key = self::getApiKey();
77
            if (empty($key)) {
78
                throw new \Exception("api_key is not configured for " . __class__);
79
            }
80
            self::$client = new SparkPostApiClient($key);
81
            if (Director::isDev()) {
82
                //@phpstan-ignore-next-line
83
                self::$client->setCurlOption(CURLOPT_VERBOSE, true);
84
            }
85
            if (Environment::getEnv("SPARKPOST_EU")) {
86
                self::$client->setEuEndpoint(true);
87
            }
88
            $subaccountId = self::config()->subaccount_id;
89
            if ($subaccountId) {
90
                self::$client->setSubaccount($subaccountId);
91
            }
92
        }
93
        return self::$client;
94
    }
95
96
    /**
97
     * Get the api client instance
98
     * @return \LeKoala\SparkPost\Api\SparkPostApiClient
99
     * @throws Exception
100
     */
101
    public static function getMasterClient()
102
    {
103
        $masterKey = self::config()->master_api_key;
104
        if (!$masterKey) {
105
            return self::getClient();
106
        }
107
        $client = new SparkPostApiClient($masterKey);
108
        return $client;
109
    }
110
111
    /**
112
     * Get the log folder and create it if necessary
113
     *
114
     * @return string
115
     */
116
    public static function getLogFolder()
117
    {
118
        $logFolder = BASE_PATH . '/' . self::config()->log_folder;
119
        if (!is_dir($logFolder)) {
120
            mkdir($logFolder, 0755, true);
121
        }
122
        return $logFolder;
123
    }
124
125
126
    /**
127
     * Process environment variable to configure this module
128
     *
129
     * @return void
130
     */
131
    public static function init()
132
    {
133
        // Regular api key used for sending emails (including subaccount support)
134
        $api_key = self::getEnvApiKey();
135
        if ($api_key) {
136
            self::config()->api_key = $api_key;
137
        }
138
139
        // Master api key that is used to configure the account. If no api key is defined, the master api key is used
140
        $master_api_key = self::getEnvMasterApiKey();
141
        if ($master_api_key) {
142
            self::config()->master_api_key = $master_api_key;
143
            if (!self::config()->api_key) {
144
                self::config()->api_key = $master_api_key;
145
            }
146
        }
147
148
        $sending_disabled = self::getEnvSendingDisabled();
149
        if ($sending_disabled === false) {
150
            // In dev, if we didn't set a value, disable by default
151
            // This can avoid sending emails by mistake :-) oops!
152
            if (Director::isDev() && !self::hasEnvSendingDisabled()) {
153
                $sending_disabled = true;
154
            }
155
        }
156
        if ($sending_disabled) {
157
            self::config()->disable_sending = $sending_disabled;
158
        }
159
        $enable_logging = self::getEnvEnableLogging();
160
        if ($enable_logging) {
161
            self::config()->enable_logging = $enable_logging;
162
        }
163
        $subaccount_id = self::getEnvSubaccountId();
164
        if ($subaccount_id) {
165
            self::config()->subaccount_id = $subaccount_id;
166
        }
167
168
        // We have a key, we can register the transport
169
        if (self::config()->api_key) {
170
            self::registerTransport();
171
        }
172
    }
173
174
    /**
175
     * @return mixed
176
     */
177
    public static function getEnvApiKey()
178
    {
179
        return Environment::getEnv('SPARKPOST_API_KEY');
180
    }
181
182
    /**
183
     * @return mixed
184
     */
185
    public static function getEnvMasterApiKey()
186
    {
187
        return Environment::getEnv('SPARKPOST_MASTER_API_KEY');
188
    }
189
190
    /**
191
     * @return mixed
192
     */
193
    public static function getEnvSendingDisabled()
194
    {
195
        return Environment::getEnv('SPARKPOST_SENDING_DISABLED');
196
    }
197
198
    /**
199
     * @return bool
200
     */
201
    public static function hasEnvSendingDisabled()
202
    {
203
        return Environment::hasEnv('SPARKPOST_SENDING_DISABLED');
204
    }
205
206
    /**
207
     * @return mixed
208
     */
209
    public static function getEnvEnableLogging()
210
    {
211
        return  Environment::getEnv('SPARKPOST_ENABLE_LOGGING');
212
    }
213
214
    /**
215
     * @return mixed
216
     */
217
    public static function getEnvSubaccountId()
218
    {
219
        return  Environment::getEnv('SPARKPOST_SUBACCOUNT_ID');
220
    }
221
222
    /**
223
     * @return mixed
224
     */
225
    public static function getSubaccountId()
226
    {
227
        return self::config()->subaccount_id;
228
    }
229
230
    /**
231
     * @return mixed
232
     */
233
    public static function getEnvForceSender()
234
    {
235
        return Environment::getEnv('SPARKPOST_FORCE_SENDER');
236
    }
237
238
    /**
239
     * @return mixed
240
     */
241
    public static function getWebhookUsername()
242
    {
243
        return self::config()->webhook_username;
244
    }
245
246
    /**
247
     * @return mixed
248
     */
249
    public static function getWebhookPassword()
250
    {
251
        return self::config()->webhook_password;
252
    }
253
254
    /**
255
     * Register the transport with the client
256
     *
257
     * @return Mailer The updated mailer
258
     * @throws Exception
259
     */
260
    public static function registerTransport()
261
    {
262
        $client = self::getClient();
263
        // Make sure MailerSubscriber is registered
264
        try {
265
            $dispatcher = Injector::inst()->get(EventDispatcherInterface::class . '.mailer');
266
        } catch (Exception $e) {
267
            // It may not be set
268
            $dispatcher = null;
269
        }
270
        $transport = new SparkPostApiTransport($client, null, $dispatcher);
271
        $mailer = new Mailer($transport);
272
        Injector::inst()->registerService($mailer, MailerInterface::class);
273
        return $mailer;
274
    }
275
276
    /**
277
     * Update admin email so that we use our config email
278
     *
279
     * @return void
280
     */
281
    public static function forceAdminEmailOverride()
282
    {
283
        Config::modify()->set(Email::class, 'admin_email', self::resolveDefaultFromEmailType());
284
    }
285
286
    /**
287
     * @param string $email
288
     * @return bool
289
     */
290
    public static function isEmailSuppressed($email)
291
    {
292
        $client = self::getClient();
293
294
        $state = $client->getSuppression($email);
295
        if (empty($state)) {
296
            return false;
297
        }
298
        return true;
299
    }
300
301
    /**
302
     * @param string $email
303
     * @return void
304
     */
305
    public static function removeSuppression($email)
306
    {
307
        self::getClient()->deleteSuppression($email);
308
    }
309
310
    /**
311
     * Check if email is ready to send emails
312
     *
313
     * @param string $email
314
     * @return boolean
315
     */
316
    public static function isEmailDomainReady($email)
317
    {
318
        if (!$email) {
319
            return false;
320
        }
321
        $email = EmailUtils::get_email_from_rfc_email($email);
322
        $parts = explode("@", $email);
0 ignored issues
show
Bug introduced by
It seems like $email can also be of type null; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

322
        $parts = explode("@", /** @scrutinizer ignore-type */ $email);
Loading history...
323
        if (count($parts) != 2) {
324
            return false;
325
        }
326
        $client = SparkPostHelper::getClient();
327
        try {
328
            $domain = $client->getSendingDomain(strtolower($parts[1]));
329
        } catch (Exception $ex) {
330
            return false;
331
        }
332
        if (!$domain) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $domain of type array<mixed,mixed> is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
333
            return false;
334
        }
335
        if ($domain['status']['dkim_status'] != 'valid') {
336
            return false;
337
        }
338
        if ($domain['status']['compliance_status'] != 'valid') {
339
            return false;
340
        }
341
        if ($domain['status']['ownership_verified'] != true) {
342
            return false;
343
        }
344
        return true;
345
    }
346
347
    /**
348
     * Resolve default send from address
349
     *
350
     * Keep in mind that an email using send() without a from
351
     * will inject the admin_email. Therefore, SiteConfig
352
     * will not be used
353
     * See forceAdminEmailOverride() or use override_admin_email config
354
     *
355
     * @param string $from
356
     * @param bool $createDefault
357
     * @return string|array<string,string>|false
358
     */
359
    public static function resolveDefaultFromEmail($from = null, $createDefault = true)
360
    {
361
        $configEmail = self::getSenderFromSiteConfig();
362
        $original_from = $from;
363
        if (!empty($from)) {
364
            // We have a set email but sending from admin => override if flag is set
365
            if (self::isAdminEmail($from) && $configEmail && self::config()->override_admin_email) {
366
                return $configEmail;
367
            }
368
            // If we have a sender, validate its email
369
            $from = EmailUtils::get_email_from_rfc_email($from);
370
            if (filter_var($from, FILTER_VALIDATE_EMAIL)) {
371
                return $original_from;
372
            }
373
        }
374
        // Look in siteconfig for default sender
375
        if ($configEmail) {
376
            return $configEmail;
377
        }
378
        // Use admin email if set
379
        if ($adminEmail = Email::config()->admin_email) {
380
            if (is_array($adminEmail) && count($adminEmail) > 0) {
381
                $email = array_keys($adminEmail)[0];
382
                return [$email => $adminEmail[$email]];
383
            } elseif (is_string($adminEmail)) {
384
                return $adminEmail;
385
            }
386
        }
387
        // If we still don't have anything, create something based on the domain
388
        if ($createDefault) {
389
            return self::createDefaultEmail();
390
        }
391
        return false;
392
    }
393
394
    /**
395
     * Returns what type of default email is used
396
     *
397
     * @return string
398
     */
399
    public static function resolveDefaultFromEmailType()
400
    {
401
        // Look in siteconfig for default sender
402
        if (self::getSenderFromSiteConfig()) {
403
            return self::FROM_SITECONFIG;
404
        }
405
        // Is admin email set ?
406
        if (Email::config()->admin_email) {
407
            return self::FROM_ADMIN;
408
        }
409
        return self::FROM_DEFAULT;
410
    }
411
412
    /**
413
     * @return string|false
414
     */
415
    public static function getSenderFromSiteConfig()
416
    {
417
        if (!class_exists(SiteConfig::class)) {
418
            return false;
419
        }
420
        try {
421
            $config = SiteConfig::current_site_config();
422
            $config_field = self::config()->siteconfig_from;
423
            if ($config_field && !empty($config->$config_field)) {
424
                return $config->$config_field;
425
            }
426
        } catch (Exception $e) {
427
            // Ignore if SiteConfig is not installed
428
        }
429
        return false;
430
    }
431
432
    public static function addMetaData(Email $email, array $meta, bool $replace = false)
433
    {
434
        $key = 'X-MC-Metadata';
435
        $headers = $email->getHeaders();
436
437
        // Merge with current
438
        if (!$replace) {
439
            $current = $headers->get($key);
440
            if ($current) {
441
                $current = json_decode($current->getBodyAsString(), true);
442
                $meta = array_merge($current, $meta);
443
            }
444
        }
445
446
        // Remove and then add, to keep always one
447
        $headers->remove($key);
448
        $headers->addTextHeader($key, json_encode($meta));
449
    }
450
451
    /**
452
     * @param string $email
453
     * @return boolean
454
     */
455
    public static function isAdminEmail($email)
456
    {
457
        $admin_email = Email::config()->admin_email;
458
        if (!$admin_email && $email) {
459
            return false;
460
        }
461
        $rfc_email = EmailUtils::get_email_from_rfc_email($email);
462
        $rfc_admin_email = EmailUtils::get_email_from_rfc_email($admin_email);
463
        return $rfc_email == $rfc_admin_email;
464
    }
465
466
    /**
467
     * @param string $email
468
     * @return boolean
469
     */
470
    public static function isDefaultEmail($email)
471
    {
472
        $rfc_email = EmailUtils::get_email_from_rfc_email($email);
473
        return $rfc_email == self::createDefaultEmail();
474
    }
475
476
    /**
477
     * Resolve default send to address
478
     *
479
     * @param string|array<mixed>|null $to
480
     * @return string|array<mixed>|null
481
     */
482
    public static function resolveDefaultToEmail($to = null)
483
    {
484
        // In case of multiple recipients, do not validate anything
485
        if (is_array($to) || strpos($to, ',') !== false) {
0 ignored issues
show
Bug introduced by
It seems like $to can also be of type null; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

485
        if (is_array($to) || strpos(/** @scrutinizer ignore-type */ $to, ',') !== false) {
Loading history...
486
            return $to;
487
        }
488
        $original_to = $to;
489
        if (!empty($to)) {
490
            $to = EmailUtils::get_email_from_rfc_email($to);
491
            if (filter_var($to, FILTER_VALIDATE_EMAIL)) {
492
                return $original_to;
493
            }
494
        }
495
        $config = SiteConfig::current_site_config();
496
        $config_field = self::config()->siteconfig_to;
497
        if ($config_field && !empty($config->$config_field)) {
498
            return $config->$config_field;
499
        }
500
        if ($admin = Email::config()->admin_email) {
501
            return $admin;
502
        }
503
        return null;
504
    }
505
506
    /**
507
     * Create a sensible default address based on domain name
508
     *
509
     * @return string
510
     */
511
    public static function createDefaultEmail()
512
    {
513
        $fulldom = Director::absoluteBaseURL();
514
        $host = parse_url($fulldom, PHP_URL_HOST);
515
        if (!$host) {
516
            $host = 'localhost';
517
        }
518
        $dom = str_replace('www.', '', $host);
519
520
        return 'postmaster@' . $dom;
521
    }
522
523
    /**
524
     * Is logging enabled?
525
     *
526
     * @return bool
527
     */
528
    public static function getLoggingEnabled()
529
    {
530
        if (self::config()->get('enable_logging')) {
531
            return true;
532
        }
533
        return false;
534
    }
535
536
    /**
537
     * Is sending enabled?
538
     *
539
     * @return bool
540
     */
541
    public static function getSendingEnabled()
542
    {
543
        if (self::config()->get('disable_sending')) {
544
            return false;
545
        }
546
        return true;
547
    }
548
}
549