1 | <?php |
||||
2 | |||||
3 | /* |
||||
4 | * @copyright 2020 Mautic Contributors. All rights reserved |
||||
5 | * @author Mautic |
||||
6 | * |
||||
7 | * @link http://mautic.org |
||||
8 | * |
||||
9 | * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html |
||||
10 | * |
||||
11 | */ |
||||
12 | |||||
13 | namespace Mautic\EmailBundle\Swiftmailer\Transport; |
||||
14 | |||||
15 | use Aws\CommandPool; |
||||
16 | use Aws\Credentials\Credentials; |
||||
17 | use Aws\Exception\AwsException; |
||||
18 | use Aws\ResultInterface; |
||||
19 | use Aws\SesV2\Exception\SesV2Exception; |
||||
20 | use Aws\SesV2\SesV2Client; |
||||
21 | use Mautic\EmailBundle\Helper\MailHelper; |
||||
22 | use Mautic\EmailBundle\Helper\PlainTextMessageHelper; |
||||
23 | use Mautic\EmailBundle\Swiftmailer\Amazon\AmazonCallback; |
||||
24 | use Psr\Log\LoggerInterface; |
||||
25 | use Symfony\Component\HttpFoundation\Request; |
||||
26 | use Symfony\Component\Translation\TranslatorInterface; |
||||
27 | |||||
28 | class AmazonApiTransport extends AbstractTokenArrayTransport implements \Swift_Transport, TokenTransportInterface, CallbackTransportInterface |
||||
29 | { |
||||
30 | /** |
||||
31 | * @var string|null |
||||
32 | */ |
||||
33 | private $region; |
||||
34 | |||||
35 | /** |
||||
36 | * @var string|null |
||||
37 | */ |
||||
38 | private $username; |
||||
39 | |||||
40 | /** |
||||
41 | * @var string|null |
||||
42 | */ |
||||
43 | private $password; |
||||
44 | |||||
45 | /** |
||||
46 | * @var TranslatorInterface |
||||
47 | */ |
||||
48 | private $translator; |
||||
49 | |||||
50 | /** |
||||
51 | * @var LoggerInterface |
||||
52 | */ |
||||
53 | private $logger; |
||||
54 | |||||
55 | /** |
||||
56 | * @var SesV2Client |
||||
57 | */ |
||||
58 | private $amazonClient; |
||||
59 | |||||
60 | /** |
||||
61 | * @var AmazonCallback |
||||
62 | */ |
||||
63 | private $amazonCallback; |
||||
64 | |||||
65 | /** |
||||
66 | * @var int |
||||
67 | */ |
||||
68 | private $concurrency; |
||||
69 | |||||
70 | /** |
||||
71 | * @var Aws\CommandInterface | Psr\Http\Message\RequestInterface |
||||
72 | */ |
||||
73 | private $handler; |
||||
74 | |||||
75 | /** |
||||
76 | * @var bool |
||||
77 | */ |
||||
78 | private $debug = false; |
||||
79 | |||||
80 | /** |
||||
81 | * @param string $region |
||||
82 | * @param string $otherRegion |
||||
83 | */ |
||||
84 | public function setRegion($region, $otherRegion = null) |
||||
85 | { |
||||
86 | $this->region = ('other' === $region) ? $otherRegion : $region; |
||||
87 | } |
||||
88 | |||||
89 | /** |
||||
90 | * @return string|null |
||||
91 | */ |
||||
92 | public function getRegion() |
||||
93 | { |
||||
94 | return $this->region; |
||||
95 | } |
||||
96 | |||||
97 | /** |
||||
98 | * @param $username |
||||
99 | */ |
||||
100 | public function setUsername($username) |
||||
101 | { |
||||
102 | $this->username = $username; |
||||
103 | } |
||||
104 | |||||
105 | /** |
||||
106 | * @return string|null |
||||
107 | */ |
||||
108 | public function getUsername() |
||||
109 | { |
||||
110 | return $this->username; |
||||
111 | } |
||||
112 | |||||
113 | /** |
||||
114 | * @param $password |
||||
115 | */ |
||||
116 | public function setPassword($password) |
||||
117 | { |
||||
118 | $this->password = $password; |
||||
119 | } |
||||
120 | |||||
121 | /** |
||||
122 | * @return string|null |
||||
123 | */ |
||||
124 | public function getPassword() |
||||
125 | { |
||||
126 | return $this->password; |
||||
127 | } |
||||
128 | |||||
129 | /** |
||||
130 | * @param $handler |
||||
131 | */ |
||||
132 | public function setHandler($handler) |
||||
133 | { |
||||
134 | $this->handler = $handler; |
||||
135 | } |
||||
136 | |||||
137 | /** |
||||
138 | * @return object|null |
||||
139 | */ |
||||
140 | public function getHandler() |
||||
141 | { |
||||
142 | return $this->handler; |
||||
143 | } |
||||
144 | |||||
145 | /** |
||||
146 | * @param $debug |
||||
147 | */ |
||||
148 | public function setDebug($debug) |
||||
149 | { |
||||
150 | $this->debug = $debug; |
||||
151 | } |
||||
152 | |||||
153 | /** |
||||
154 | * @return bool|null |
||||
155 | */ |
||||
156 | public function getDebug() |
||||
157 | { |
||||
158 | return $this->debug; |
||||
159 | } |
||||
160 | |||||
161 | public function __construct( |
||||
162 | TranslatorInterface $translator, |
||||
163 | AmazonCallback $amazonCallback, |
||||
164 | LoggerInterface $logger |
||||
165 | ) { |
||||
166 | $this->amazonCallback = $amazonCallback; |
||||
167 | $this->translator = $translator; |
||||
168 | $this->logger = $logger; |
||||
169 | } |
||||
170 | |||||
171 | public function start() |
||||
172 | { |
||||
173 | if (empty($this->region) || empty($this->username) || empty($this->password)) { |
||||
174 | $this->throwException($this->translator->trans('mautic.email.api_key_required', [], 'validators')); |
||||
175 | } |
||||
176 | |||||
177 | if (!$this->started) { |
||||
178 | $this->amazonClient = $this->createAmazonClient(); |
||||
179 | |||||
180 | $account = $this->amazonClient->getAccount(); |
||||
181 | $emailQuotaRemaining = $account->get('SendQuota')['Max24HourSend'] - $account->get('SendQuota')['SentLast24Hours']; |
||||
182 | |||||
183 | if (!$account->get('SendingEnabled')) { |
||||
184 | $this->logger->error('Your AWS SES is not enabled for sending'); |
||||
185 | throw new \Exception('Your AWS SES is not enabled for sending'); |
||||
186 | } |
||||
187 | |||||
188 | if (!$account->get('ProductionAccessEnabled')) { |
||||
189 | $this->logger->info('Your AWS SES is in sandbox mode, consider moving it to production state'); |
||||
190 | } |
||||
191 | |||||
192 | if ($emailQuotaRemaining <= 0) { |
||||
193 | $this->logger->error('Your AWS SES quota is currently exceeded, used '.$account->get('SentLast24Hours').' of '.$account->get('Max24HourSend')); |
||||
194 | throw new \Exception('Your AWS SES quota is currently exceeded'); |
||||
195 | } |
||||
196 | |||||
197 | $this->concurrency = floor($account->get('SendQuota')['MaxSendRate']); |
||||
198 | |||||
199 | $this->started = true; |
||||
200 | } |
||||
201 | } |
||||
202 | |||||
203 | public function createAmazonClient() |
||||
204 | { |
||||
205 | $config = [ |
||||
206 | 'version' => '2019-09-27', |
||||
207 | 'region' => $this->region, |
||||
208 | 'credentials' => new Credentials( |
||||
209 | $this->username, |
||||
210 | $this->password |
||||
211 | ), |
||||
212 | ]; |
||||
213 | |||||
214 | if ($this->handler) { |
||||
215 | $config['handler'] = $this->handler; |
||||
216 | } |
||||
217 | |||||
218 | if ($this->debug) { |
||||
219 | $config['debug'] = [ |
||||
220 | 'logfn' => function ($msg) { |
||||
221 | $this->logger->debug($msg); |
||||
222 | }, |
||||
223 | 'http' => true, |
||||
224 | 'stream_size' => '0', |
||||
225 | ]; |
||||
226 | } |
||||
227 | |||||
228 | return new SesV2Client($config); |
||||
229 | } |
||||
230 | |||||
231 | /** |
||||
232 | * @param null $failedRecipients |
||||
233 | * |
||||
234 | * @return int Number of messages sent |
||||
235 | * |
||||
236 | * @throws \Exception |
||||
237 | */ |
||||
238 | public function send(\Swift_Mime_SimpleMessage $toSendMessage, &$failedRecipients = null) |
||||
239 | { |
||||
240 | $this->message = $toSendMessage; |
||||
241 | $failedRecipients = (array) $failedRecipients; |
||||
242 | |||||
243 | if ($evt = $this->getDispatcher()->createSendEvent($this, $toSendMessage)) { |
||||
244 | $this->getDispatcher()->dispatchEvent($evt, 'beforeSendPerformed'); |
||||
245 | if ($evt->bubbleCancelled()) { |
||||
246 | return 0; |
||||
247 | } |
||||
248 | } |
||||
249 | $count = $this->getBatchRecipientCount($toSendMessage); |
||||
250 | |||||
251 | try { |
||||
252 | $this->start(); |
||||
253 | $commands = []; |
||||
254 | foreach ($this->getAmazonMessage($toSendMessage) as $rawEmail) { |
||||
255 | $commands[] = $this->amazonClient->getCommand('sendEmail', $rawEmail); |
||||
256 | } |
||||
257 | $pool = new CommandPool($this->amazonClient, $commands, [ |
||||
258 | 'concurrency' => $this->concurrency, |
||||
259 | 'fulfilled' => function (ResultInterface $result, $iteratorId) use ($evt, $failedRecipients) { |
||||
260 | if ($evt) { |
||||
0 ignored issues
–
show
introduced
by
Loading history...
|
|||||
261 | // $this->logger->info("SES Result: " . $result->get('MessageId')); |
||||
262 | $evt->setResult(\Swift_Events_SendEvent::RESULT_SUCCESS); |
||||
263 | $evt->setFailedRecipients($failedRecipients); |
||||
264 | $this->getDispatcher()->dispatchEvent($evt, 'sendPerformed'); |
||||
265 | } |
||||
266 | }, |
||||
267 | 'rejected' => function (AwsException $reason, $iteratorId) use ($evt) { |
||||
0 ignored issues
–
show
The parameter
$iteratorId 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
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...
|
|||||
268 | $failedRecipients = []; |
||||
269 | $this->triggerSendError($evt, $failedRecipients, $reason->getAwsErrorMessage()); |
||||
270 | }, |
||||
271 | ]); |
||||
272 | $promise = $pool->promise(); |
||||
273 | $promise->wait(); |
||||
274 | |||||
275 | return count($commands); |
||||
276 | } catch (SesV2Exception $e) { |
||||
277 | $this->triggerSendError($evt, $failedRecipients, $e->getMessage()); |
||||
278 | $this->throwException($e->getMessage()); |
||||
279 | } catch (\Exception $e) { |
||||
280 | $this->triggerSendError($evt, $failedRecipients, $e->getMessage()); |
||||
281 | $this->throwException($e->getMessage()); |
||||
282 | } |
||||
283 | |||||
284 | return 1; |
||||
285 | } |
||||
286 | |||||
287 | private function triggerSendError(\Swift_Events_SendEvent $evt, &$failedRecipients, $reason) |
||||
288 | { |
||||
289 | $this->logger->error('SES API Error: '.$reason); |
||||
290 | |||||
291 | $failedRecipients = array_merge( |
||||
292 | $failedRecipients, |
||||
293 | array_keys((array) $this->message->getTo()), |
||||
294 | array_keys((array) $this->message->getCc()), |
||||
295 | array_keys((array) $this->message->getBcc()) |
||||
296 | ); |
||||
297 | |||||
298 | if ($evt) { |
||||
0 ignored issues
–
show
|
|||||
299 | $evt->setResult(\Swift_Events_SendEvent::RESULT_FAILED); |
||||
300 | $evt->setFailedRecipients($failedRecipients); |
||||
301 | $this->getDispatcher()->dispatchEvent($evt, 'sendPerformed'); |
||||
302 | } |
||||
303 | } |
||||
304 | |||||
305 | /* |
||||
306 | * @return array Amazon Send Message |
||||
307 | * |
||||
308 | * @throws \Exception |
||||
309 | */ |
||||
310 | public function getAmazonMessage(\Swift_Mime_SimpleMessage $message) |
||||
311 | { |
||||
312 | /** |
||||
313 | * Three ways to send the email |
||||
314 | * Simple: A standard email message. When you create this type of message, you specify the sender, the recipient, and the message body, and Amazon SES assembles the message for you. |
||||
315 | * Raw : A raw, MIME-formatted email message. When you send this type of email, you have to specify all of the message headers, as well as the message body. You can use this message type to send messages that contain attachments. The message that you specify has to be a valid MIME message. |
||||
316 | * Template: A message that contains personalization tags. When you send this type of email, Amazon SES API v2 automatically replaces the tags with values that you specify. |
||||
317 | * |
||||
318 | * In Mautic we need to use RAW all the time because we inject custom headers all the time, so templates and simple are not useful |
||||
319 | * If in the future AWS allow custom headers in templates or simple emails we should consider them |
||||
320 | * |
||||
321 | * Since we are using SES RAW method, we need to create a seperate message each time we send out an email |
||||
322 | * In case SES changes their API this is the only function that needs to be changed to accomrdate sending a template |
||||
323 | */ |
||||
324 | $this->message = $message; |
||||
0 ignored issues
–
show
$message is of type Swift_Mime_SimpleMessage , but the property $message was declared to be of type Swift_Message . Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly. Either this assignment is in error or an instanceof check should be added for that assignment. class Alien {}
class Dalek extends Alien {}
class Plot
{
/** @var Dalek */
public $villain;
}
$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
$plot->villain = $alien;
}
Loading history...
|
|||||
325 | $metadata = $this->getMetadata(); |
||||
326 | $emailBody = $this->message->getBody(); |
||||
327 | $emailSubject = $this->message->getSubject(); |
||||
328 | $emailText = PlainTextMessageHelper::getPlainTextFromMessage($message); |
||||
329 | $sesArray = []; |
||||
330 | |||||
331 | if (empty($metadata)) { |
||||
332 | /** |
||||
333 | * This is a queued message, all the information are included |
||||
334 | * in the $message object |
||||
335 | * just construct the $sesArray. |
||||
336 | */ |
||||
337 | $from = $message->getFrom(); |
||||
338 | $fromEmail = current(array_keys($from)); |
||||
339 | $fromName = $from[$fromEmail]; |
||||
340 | |||||
341 | $sesArray['FromEmailAddress'] = (!empty($fromName)) ? $fromName.' <'.$fromEmail.'>' : $fromEmail; |
||||
342 | $to = $message->getTo(); |
||||
343 | if (!empty($to)) { |
||||
344 | $sesArray['Destination']['ToAddresses'] = array_keys($to); |
||||
345 | } |
||||
346 | |||||
347 | $cc = $message->getCc(); |
||||
348 | if (!empty($cc)) { |
||||
349 | $sesArray['Destination']['CcAddresses'] = array_keys($cc); |
||||
350 | } |
||||
351 | $bcc = $message->getBcc(); |
||||
352 | if (!empty($bcc)) { |
||||
353 | $sesArray['Destination']['BccAddresses'] = array_keys($bcc); |
||||
354 | } |
||||
355 | $replyTo = $message->getReplyTo(); |
||||
356 | if (!empty($replyTo)) { |
||||
357 | $sesArray['ReplyToAddresses'] = [key($replyTo)]; |
||||
358 | } |
||||
359 | $headers = $message->getHeaders(); |
||||
360 | if ($headers->has('X-SES-CONFIGURATION-SET')) { |
||||
361 | $sesArray['ConfigurationSetName'] = $headers->get('X-SES-CONFIGURATION-SET'); |
||||
362 | } |
||||
363 | |||||
364 | $sesArray['Content']['Raw']['Data'] = $message->toString(); |
||||
365 | yield $sesArray; |
||||
366 | } else { |
||||
367 | /** |
||||
368 | * This is a message with tokens. |
||||
369 | */ |
||||
370 | $mauticTokens = []; |
||||
371 | $metadataSet = reset($metadata); |
||||
372 | $tokens = (!empty($metadataSet['tokens'])) ? $metadataSet['tokens'] : []; |
||||
373 | $mauticTokens = array_keys($tokens); |
||||
374 | foreach ($metadata as $recipient => $mailData) { |
||||
375 | // Reset the parts of the email that has tokens |
||||
376 | $this->message->setSubject($emailSubject); |
||||
377 | $this->message->setBody($emailBody); |
||||
378 | $this->setPlainTextToMessage($this->message, $emailText); |
||||
379 | // Convert the message to array to get the values |
||||
380 | $tokenizedMessage = $this->messageToArray($mauticTokens, $mailData['tokens'], false); |
||||
381 | $toSendMessage = (new \Swift_Message()); |
||||
382 | $toSendMessage->setSubject($tokenizedMessage['subject']); |
||||
383 | $toSendMessage->setFrom([$tokenizedMessage['from']['email'] => $tokenizedMessage['from']['name']]); |
||||
384 | $sesArray['FromEmailAddress'] = (!empty($tokenizedMessage['from']['name'])) ? $tokenizedMessage['from']['name'].' <'.$tokenizedMessage['from']['email'].'>' : $tokenizedMessage['from']['email']; |
||||
385 | $toSendMessage->setTo([$recipient]); |
||||
386 | $sesArray['Destination']['ToAddresses'] = [$recipient]; |
||||
387 | if (isset($tokenizedMessage['text']) && strlen($tokenizedMessage['text']) > 0) { |
||||
388 | $toSendMessage->addPart($tokenizedMessage['text'], 'text/plain'); |
||||
389 | } |
||||
390 | |||||
391 | if (isset($tokenizedMessage['html']) && strlen($tokenizedMessage['html']) > 0) { |
||||
392 | $toSendMessage->addPart($tokenizedMessage['html'], 'text/html'); |
||||
393 | } |
||||
394 | if (isset($tokenizedMessage['headers'])) { |
||||
395 | $headers = $toSendMessage->getHeaders(); |
||||
396 | foreach ($tokenizedMessage['headers'] as $key => $value) { |
||||
397 | $headers->addTextHeader($key, $value); |
||||
398 | } |
||||
399 | } |
||||
400 | |||||
401 | if (count($tokenizedMessage['recipients']['cc']) > 0) { |
||||
402 | $cc = array_keys($tokenizedMessage['recipients']['cc']); |
||||
403 | $toSendMessage->setCc($cc); |
||||
404 | $sesArray['Destination']['CcAddresses'] = $cc; |
||||
405 | } |
||||
406 | |||||
407 | if (count($tokenizedMessage['recipients']['bcc']) > 0) { |
||||
408 | $bcc = array_keys($tokenizedMessage['recipients']['bcc']); |
||||
409 | $toSendMessage->setBcc($bcc); |
||||
410 | $sesArray['Destination']['BccAddresses'] = $bcc; |
||||
411 | } |
||||
412 | |||||
413 | if (isset($tokenizedMessage['replyTo'])) { |
||||
414 | $toSendMessage->setReplyTo([$tokenizedMessage['replyTo']['email']]); |
||||
415 | $sesArray['ReplyToAddresses'] = [$tokenizedMessage['replyTo']['email']]; |
||||
416 | } |
||||
417 | if (isset($tokenizedMessage['headers']['X-SES-CONFIGURATION-SET'])) { |
||||
418 | $sesArray['ConfigurationSetName'] = $tokenizedMessage['headers']['X-SES-CONFIGURATION-SET']; |
||||
419 | } |
||||
420 | |||||
421 | if (count($tokenizedMessage['file_attachments']) > 0) { |
||||
422 | foreach ($tokenizedMessage['file_attachments'] as $attachment) { |
||||
423 | $fileAttach = \Swift_Attachment::fromPath($attachment['filePath']); |
||||
424 | $fileAttach->setFilename($attachment['fileName']); |
||||
425 | $fileAttach->setContentType($attachment['contentType']); |
||||
426 | $toSendMessage->attach($fileAttach); |
||||
427 | } |
||||
428 | } |
||||
429 | if (count($tokenizedMessage['binary_attachments']) > 0) { |
||||
430 | foreach ($tokenizedMessage['binary_attachments'] as $attachment) { |
||||
431 | $fileAttach = new \Swift_Attachment($attachment['content'], $attachment['name'], $attachment['type']); |
||||
432 | $toSendMessage->attach($fileAttach); |
||||
433 | } |
||||
434 | } |
||||
435 | $sesArray['Content']['Raw']['Data'] = $toSendMessage->toString(); |
||||
436 | yield $sesArray; |
||||
437 | } |
||||
438 | } |
||||
439 | } |
||||
440 | |||||
441 | /** |
||||
442 | * Set plain text to a message. |
||||
443 | * |
||||
444 | * @return bool |
||||
445 | */ |
||||
446 | private function setPlainTextToMessage(\Swift_Mime_SimpleMessage $message, $text) |
||||
447 | { |
||||
448 | $children = (array) $message->getChildren(); |
||||
449 | |||||
450 | foreach ($children as $child) { |
||||
451 | $childType = $child->getContentType(); |
||||
452 | if ('text/plain' === $childType && $child instanceof \Swift_MimePart) { |
||||
453 | $child->setBody($text); |
||||
454 | } |
||||
455 | } |
||||
456 | } |
||||
457 | |||||
458 | /** |
||||
459 | * @return int |
||||
460 | */ |
||||
461 | public function getMaxBatchLimit() |
||||
462 | { |
||||
463 | return 0; |
||||
464 | } |
||||
465 | |||||
466 | /** |
||||
467 | * @param int $toBeAdded |
||||
468 | * @param string $type |
||||
469 | */ |
||||
470 | public function getBatchRecipientCount(\Swift_Message $toSendMessage, $toBeAdded = 1, $type = 'to'): int |
||||
471 | { |
||||
472 | // These getters could return null |
||||
473 | $toCount = $toSendMessage->getTo() ? count($toSendMessage->getTo()) : 0; |
||||
474 | $ccCount = $toSendMessage->getCc() ? count($toSendMessage->getCc()) : 0; |
||||
475 | $bccCount = $toSendMessage->getBcc() ? count($toSendMessage->getBcc()) : 0; |
||||
476 | |||||
477 | return $toCount + $ccCount + $bccCount + $toBeAdded; |
||||
478 | } |
||||
479 | |||||
480 | /** |
||||
481 | * Returns a "transport" string to match the URL path /mailer/{transport}/callback. |
||||
482 | * |
||||
483 | * @return mixed |
||||
484 | */ |
||||
485 | public function getCallbackPath() |
||||
486 | { |
||||
487 | return 'amazon_api'; |
||||
488 | } |
||||
489 | |||||
490 | /** |
||||
491 | * Handle response. |
||||
492 | */ |
||||
493 | public function processCallbackRequest(Request $request) |
||||
494 | { |
||||
495 | $this->amazonCallback->processCallbackRequest($request); |
||||
496 | } |
||||
497 | |||||
498 | /** |
||||
499 | * @return bool |
||||
500 | */ |
||||
501 | public function ping() |
||||
502 | { |
||||
503 | return true; |
||||
504 | } |
||||
505 | } |
||||
506 |