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
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* This configurable class helps decoupling the api client from SilverStripe |
21
|
|
|
*/ |
22
|
|
|
class SparkPostHelper |
23
|
|
|
{ |
24
|
|
|
use Configurable; |
25
|
|
|
|
26
|
|
|
const FROM_SITECONFIG = "SiteConfig"; |
27
|
|
|
const FROM_ADMIN = "Admin"; |
28
|
|
|
const FROM_DEFAULT = "Default"; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* Client instance |
32
|
|
|
* |
33
|
|
|
* @var ?\LeKoala\SparkPost\Api\SparkPostApiClient |
34
|
|
|
*/ |
35
|
|
|
protected static $client; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* Get the mailer instance |
39
|
|
|
* |
40
|
|
|
* @return MailerInterface |
41
|
|
|
*/ |
42
|
|
|
public static function getMailer() |
43
|
|
|
{ |
44
|
|
|
return Injector::inst()->get(MailerInterface::class); |
45
|
|
|
} |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* @param MailerInterface $mailer |
49
|
|
|
* @return \Symfony\Component\Mailer\Transport\AbstractTransport|SparkPostApiTransport |
50
|
|
|
*/ |
51
|
|
|
public static function getTransportFromMailer($mailer) |
52
|
|
|
{ |
53
|
|
|
$r = new ReflectionObject($mailer); |
54
|
|
|
$p = $r->getProperty('transport'); |
55
|
|
|
$p->setAccessible(true); |
56
|
|
|
return $p->getValue($mailer); |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* @return string |
61
|
|
|
*/ |
62
|
|
|
public static function getApiKey() |
63
|
|
|
{ |
64
|
|
|
return self::config()->api_key; |
65
|
|
|
} |
66
|
|
|
|
67
|
|
|
/** |
68
|
|
|
* Get the api client instance |
69
|
|
|
* @return SparkPostApiClient |
70
|
|
|
* @throws Exception |
71
|
|
|
*/ |
72
|
|
|
public static function getClient() |
73
|
|
|
{ |
74
|
|
|
if (!self::$client) { |
75
|
|
|
$key = self::getApiKey(); |
76
|
|
|
if (empty($key)) { |
77
|
|
|
throw new \Exception("api_key is not configured for " . __class__); |
78
|
|
|
} |
79
|
|
|
self::$client = new SparkPostApiClient($key); |
80
|
|
|
if (Director::isDev()) { |
81
|
|
|
//@phpstan-ignore-next-line |
82
|
|
|
self::$client->setCurlOption(CURLOPT_VERBOSE, true); |
83
|
|
|
} |
84
|
|
|
if (Environment::getEnv("SPARKPOST_EU")) { |
85
|
|
|
self::$client->setEuEndpoint(true); |
86
|
|
|
} |
87
|
|
|
$subaccountId = self::config()->subaccount_id; |
88
|
|
|
if ($subaccountId) { |
89
|
|
|
self::$client->setSubaccount($subaccountId); |
90
|
|
|
} |
91
|
|
|
} |
92
|
|
|
return self::$client; |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* Get the api client instance |
97
|
|
|
* @return \LeKoala\SparkPost\Api\SparkPostApiClient |
98
|
|
|
* @throws Exception |
99
|
|
|
*/ |
100
|
|
|
public static function getMasterClient() |
101
|
|
|
{ |
102
|
|
|
$masterKey = self::config()->master_api_key; |
103
|
|
|
if (!$masterKey) { |
104
|
|
|
return self::getClient(); |
105
|
|
|
} |
106
|
|
|
$client = new SparkPostApiClient($masterKey); |
107
|
|
|
return $client; |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
/** |
111
|
|
|
* Get the log folder and create it if necessary |
112
|
|
|
* |
113
|
|
|
* @return string |
114
|
|
|
*/ |
115
|
|
|
public static function getLogFolder() |
116
|
|
|
{ |
117
|
|
|
$logFolder = BASE_PATH . '/' . self::config()->log_folder; |
118
|
|
|
if (!is_dir($logFolder)) { |
119
|
|
|
mkdir($logFolder, 0755, true); |
120
|
|
|
} |
121
|
|
|
return $logFolder; |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
|
125
|
|
|
/** |
126
|
|
|
* Process environment variable to configure this module |
127
|
|
|
* |
128
|
|
|
* @return void |
129
|
|
|
*/ |
130
|
|
|
public static function init() |
131
|
|
|
{ |
132
|
|
|
// Regular api key used for sending emails (including subaccount support) |
133
|
|
|
$api_key = self::getEnvApiKey(); |
134
|
|
|
if ($api_key) { |
135
|
|
|
self::config()->api_key = $api_key; |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
// Master api key that is used to configure the account. If no api key is defined, the master api key is used |
139
|
|
|
$master_api_key = self::getEnvMasterApiKey(); |
140
|
|
|
if ($master_api_key) { |
141
|
|
|
self::config()->master_api_key = $master_api_key; |
142
|
|
|
if (!self::config()->api_key) { |
143
|
|
|
self::config()->api_key = $master_api_key; |
144
|
|
|
} |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
$sending_disabled = self::getEnvSendingDisabled(); |
148
|
|
|
if ($sending_disabled) { |
149
|
|
|
self::config()->disable_sending = $sending_disabled; |
150
|
|
|
} |
151
|
|
|
$enable_logging = self::getEnvEnableLogging(); |
152
|
|
|
if ($enable_logging) { |
153
|
|
|
self::config()->enable_logging = $enable_logging; |
154
|
|
|
} |
155
|
|
|
$subaccount_id = self::getEnvSubaccountId(); |
156
|
|
|
if ($subaccount_id) { |
157
|
|
|
self::config()->subaccount_id = $subaccount_id; |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
// We have a key, we can register the transport |
161
|
|
|
if (self::config()->api_key) { |
162
|
|
|
self::registerTransport(); |
163
|
|
|
} |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
/** |
167
|
|
|
* @return mixed |
168
|
|
|
*/ |
169
|
|
|
public static function getEnvApiKey() |
170
|
|
|
{ |
171
|
|
|
return Environment::getEnv('SPARKPOST_API_KEY'); |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
/** |
175
|
|
|
* @return mixed |
176
|
|
|
*/ |
177
|
|
|
public static function getEnvMasterApiKey() |
178
|
|
|
{ |
179
|
|
|
return Environment::getEnv('SPARKPOST_MASTER_API_KEY'); |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
/** |
183
|
|
|
* @return mixed |
184
|
|
|
*/ |
185
|
|
|
public static function getEnvSendingDisabled() |
186
|
|
|
{ |
187
|
|
|
return Environment::getEnv('SPARKPOST_SENDING_DISABLED'); |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* @return mixed |
192
|
|
|
*/ |
193
|
|
|
public static function getEnvEnableLogging() |
194
|
|
|
{ |
195
|
|
|
return Environment::getEnv('SPARKPOST_ENABLE_LOGGING'); |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
/** |
199
|
|
|
* @return mixed |
200
|
|
|
*/ |
201
|
|
|
public static function getEnvSubaccountId() |
202
|
|
|
{ |
203
|
|
|
return Environment::getEnv('SPARKPOST_SUBACCOUNT_ID'); |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
/** |
207
|
|
|
* @return mixed |
208
|
|
|
*/ |
209
|
|
|
public static function getSubaccountId() |
210
|
|
|
{ |
211
|
|
|
return self::config()->subaccount_id; |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
/** |
215
|
|
|
* @return mixed |
216
|
|
|
*/ |
217
|
|
|
public static function getEnvForceSender() |
218
|
|
|
{ |
219
|
|
|
return Environment::getEnv('SPARKPOST_FORCE_SENDER'); |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
/** |
223
|
|
|
* @return mixed |
224
|
|
|
*/ |
225
|
|
|
public static function getWebhookUsername() |
226
|
|
|
{ |
227
|
|
|
return self::config()->webhook_username; |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
/** |
231
|
|
|
* @return mixed |
232
|
|
|
*/ |
233
|
|
|
public static function getWebhookPassword() |
234
|
|
|
{ |
235
|
|
|
return self::config()->webhook_password; |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
/** |
239
|
|
|
* Register the transport with the client |
240
|
|
|
* |
241
|
|
|
* @return Mailer The updated mailer |
242
|
|
|
* @throws Exception |
243
|
|
|
*/ |
244
|
|
|
public static function registerTransport() |
245
|
|
|
{ |
246
|
|
|
$client = self::getClient(); |
247
|
|
|
$transport = new SparkPostApiTransport($client); |
248
|
|
|
$mailer = new Mailer($transport); |
249
|
|
|
Injector::inst()->registerService($mailer, MailerInterface::class); |
250
|
|
|
return $mailer; |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
/** |
254
|
|
|
* Update admin email so that we use our config email |
255
|
|
|
* |
256
|
|
|
* @return void |
257
|
|
|
*/ |
258
|
|
|
public static function forceAdminEmailOverride() |
259
|
|
|
{ |
260
|
|
|
Config::modify()->set(Email::class, 'admin_email', self::resolveDefaultFromEmailType()); |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
/** |
264
|
|
|
* @param string $email |
265
|
|
|
* @return bool |
266
|
|
|
*/ |
267
|
|
|
public static function isEmailSuppressed($email) |
268
|
|
|
{ |
269
|
|
|
$client = self::getClient(); |
270
|
|
|
|
271
|
|
|
$state = $client->getSuppression($email); |
272
|
|
|
if (empty($state)) { |
273
|
|
|
return false; |
274
|
|
|
} |
275
|
|
|
return true; |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
/** |
279
|
|
|
* @param string $email |
280
|
|
|
* @return void |
281
|
|
|
*/ |
282
|
|
|
public static function removeSuppression($email) |
283
|
|
|
{ |
284
|
|
|
self::getClient()->deleteSuppression($email); |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
/** |
288
|
|
|
* Check if email is ready to send emails |
289
|
|
|
* |
290
|
|
|
* @param string $email |
291
|
|
|
* @return boolean |
292
|
|
|
*/ |
293
|
|
|
public static function isEmailDomainReady($email) |
294
|
|
|
{ |
295
|
|
|
if (!$email) { |
296
|
|
|
return false; |
297
|
|
|
} |
298
|
|
|
$parts = explode("@", $email); |
299
|
|
|
if (count($parts) != 2) { |
300
|
|
|
return false; |
301
|
|
|
} |
302
|
|
|
$client = SparkPostHelper::getClient(); |
303
|
|
|
try { |
304
|
|
|
$domain = $client->getSendingDomain(strtolower($parts[1])); |
305
|
|
|
} catch (Exception $ex) { |
306
|
|
|
return false; |
307
|
|
|
} |
308
|
|
|
if (!$domain) { |
|
|
|
|
309
|
|
|
return false; |
310
|
|
|
} |
311
|
|
|
if ($domain['status']['dkim_status'] != 'valid') { |
312
|
|
|
return false; |
313
|
|
|
} |
314
|
|
|
if ($domain['status']['compliance_status'] != 'valid') { |
315
|
|
|
return false; |
316
|
|
|
} |
317
|
|
|
if ($domain['status']['ownership_verified'] != true) { |
318
|
|
|
return false; |
319
|
|
|
} |
320
|
|
|
return true; |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
/** |
324
|
|
|
* Resolve default send from address |
325
|
|
|
* |
326
|
|
|
* Keep in mind that an email using send() without a from |
327
|
|
|
* will inject the admin_email. Therefore, SiteConfig |
328
|
|
|
* will not be used |
329
|
|
|
* See forceAdminEmailOverride() or use override_admin_email config |
330
|
|
|
* |
331
|
|
|
* @param string $from |
332
|
|
|
* @param bool $createDefault |
333
|
|
|
* @return string|array<string,string>|false |
334
|
|
|
*/ |
335
|
|
|
public static function resolveDefaultFromEmail($from = null, $createDefault = true) |
336
|
|
|
{ |
337
|
|
|
$configEmail = self::getSenderFromSiteConfig(); |
338
|
|
|
$original_from = $from; |
339
|
|
|
if (!empty($from)) { |
340
|
|
|
// We have a set email but sending from admin => override if flag is set |
341
|
|
|
if (self::isAdminEmail($from) && $configEmail && self::config()->override_admin_email) { |
342
|
|
|
return $configEmail; |
343
|
|
|
} |
344
|
|
|
// If we have a sender, validate its email |
345
|
|
|
$from = EmailUtils::get_email_from_rfc_email($from); |
346
|
|
|
if (filter_var($from, FILTER_VALIDATE_EMAIL)) { |
347
|
|
|
return $original_from; |
348
|
|
|
} |
349
|
|
|
} |
350
|
|
|
// Look in siteconfig for default sender |
351
|
|
|
if ($configEmail) { |
352
|
|
|
return $configEmail; |
353
|
|
|
} |
354
|
|
|
// Use admin email if set |
355
|
|
|
if ($adminEmail = Email::config()->admin_email) { |
356
|
|
|
if (is_array($adminEmail) && count($adminEmail) > 0) { |
357
|
|
|
$email = array_keys($adminEmail)[0]; |
358
|
|
|
return [$email => $adminEmail[$email]]; |
359
|
|
|
} elseif (is_string($adminEmail)) { |
360
|
|
|
return $adminEmail; |
361
|
|
|
} |
362
|
|
|
} |
363
|
|
|
// If we still don't have anything, create something based on the domain |
364
|
|
|
if ($createDefault) { |
365
|
|
|
return self::createDefaultEmail(); |
366
|
|
|
} |
367
|
|
|
return false; |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
/** |
371
|
|
|
* Returns what type of default email is used |
372
|
|
|
* |
373
|
|
|
* @return string |
374
|
|
|
*/ |
375
|
|
|
public static function resolveDefaultFromEmailType() |
376
|
|
|
{ |
377
|
|
|
// Look in siteconfig for default sender |
378
|
|
|
if (self::getSenderFromSiteConfig()) { |
379
|
|
|
return self::FROM_SITECONFIG; |
380
|
|
|
} |
381
|
|
|
// Is admin email set ? |
382
|
|
|
if (Email::config()->admin_email) { |
383
|
|
|
return self::FROM_ADMIN; |
384
|
|
|
} |
385
|
|
|
return self::FROM_DEFAULT; |
386
|
|
|
} |
387
|
|
|
|
388
|
|
|
/** |
389
|
|
|
* @return string|false |
390
|
|
|
*/ |
391
|
|
|
public static function getSenderFromSiteConfig() |
392
|
|
|
{ |
393
|
|
|
$config = SiteConfig::current_site_config(); |
394
|
|
|
$config_field = self::config()->siteconfig_from; |
395
|
|
|
if ($config_field && !empty($config->$config_field)) { |
396
|
|
|
return $config->$config_field; |
397
|
|
|
} |
398
|
|
|
return false; |
399
|
|
|
} |
400
|
|
|
|
401
|
|
|
/** |
402
|
|
|
* @param string $email |
403
|
|
|
* @return boolean |
404
|
|
|
*/ |
405
|
|
|
public static function isAdminEmail($email) |
406
|
|
|
{ |
407
|
|
|
$admin_email = Email::config()->admin_email; |
408
|
|
|
if (!$admin_email && $email) { |
409
|
|
|
return false; |
410
|
|
|
} |
411
|
|
|
$rfc_email = EmailUtils::get_email_from_rfc_email($email); |
412
|
|
|
$rfc_admin_email = EmailUtils::get_email_from_rfc_email($admin_email); |
413
|
|
|
return $rfc_email == $rfc_admin_email; |
414
|
|
|
} |
415
|
|
|
|
416
|
|
|
/** |
417
|
|
|
* @param string $email |
418
|
|
|
* @return boolean |
419
|
|
|
*/ |
420
|
|
|
public static function isDefaultEmail($email) |
421
|
|
|
{ |
422
|
|
|
$rfc_email = EmailUtils::get_email_from_rfc_email($email); |
423
|
|
|
return $rfc_email == self::createDefaultEmail(); |
424
|
|
|
} |
425
|
|
|
|
426
|
|
|
/** |
427
|
|
|
* Resolve default send to address |
428
|
|
|
* |
429
|
|
|
* @param string|array<mixed>|null $to |
430
|
|
|
* @return string|array<mixed>|null |
431
|
|
|
*/ |
432
|
|
|
public static function resolveDefaultToEmail($to = null) |
433
|
|
|
{ |
434
|
|
|
// In case of multiple recipients, do not validate anything |
435
|
|
|
if (is_array($to) || strpos($to, ',') !== false) { |
|
|
|
|
436
|
|
|
return $to; |
437
|
|
|
} |
438
|
|
|
$original_to = $to; |
439
|
|
|
if (!empty($to)) { |
440
|
|
|
$to = EmailUtils::get_email_from_rfc_email($to); |
441
|
|
|
if (filter_var($to, FILTER_VALIDATE_EMAIL)) { |
442
|
|
|
return $original_to; |
443
|
|
|
} |
444
|
|
|
} |
445
|
|
|
$config = SiteConfig::current_site_config(); |
446
|
|
|
$config_field = self::config()->siteconfig_to; |
447
|
|
|
if ($config_field && !empty($config->$config_field)) { |
448
|
|
|
return $config->$config_field; |
449
|
|
|
} |
450
|
|
|
if ($admin = Email::config()->admin_email) { |
451
|
|
|
return $admin; |
452
|
|
|
} |
453
|
|
|
return null; |
454
|
|
|
} |
455
|
|
|
|
456
|
|
|
/** |
457
|
|
|
* Create a sensible default address based on domain name |
458
|
|
|
* |
459
|
|
|
* @return string |
460
|
|
|
*/ |
461
|
|
|
public static function createDefaultEmail() |
462
|
|
|
{ |
463
|
|
|
$fulldom = Director::absoluteBaseURL(); |
464
|
|
|
$host = parse_url($fulldom, PHP_URL_HOST); |
465
|
|
|
if (!$host) { |
466
|
|
|
$host = 'localhost'; |
467
|
|
|
} |
468
|
|
|
$dom = str_replace('www.', '', $host); |
469
|
|
|
|
470
|
|
|
return 'postmaster@' . $dom; |
471
|
|
|
} |
472
|
|
|
|
473
|
|
|
/** |
474
|
|
|
* Is logging enabled? |
475
|
|
|
* |
476
|
|
|
* @return bool |
477
|
|
|
*/ |
478
|
|
|
public static function getLoggingEnabled() |
479
|
|
|
{ |
480
|
|
|
if (self::config()->get('enable_logging')) { |
481
|
|
|
return true; |
482
|
|
|
} |
483
|
|
|
return false; |
484
|
|
|
} |
485
|
|
|
|
486
|
|
|
/** |
487
|
|
|
* Is sending enabled? |
488
|
|
|
* |
489
|
|
|
* @return bool |
490
|
|
|
*/ |
491
|
|
|
public static function getSendingEnabled() |
492
|
|
|
{ |
493
|
|
|
if (self::config()->get('disable_sending')) { |
494
|
|
|
return false; |
495
|
|
|
} |
496
|
|
|
return true; |
497
|
|
|
} |
498
|
|
|
} |
499
|
|
|
|
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.