From 3b9d50d949df57859add3d07c6fe7f1dc14f2c42 Mon Sep 17 00:00:00 2001 From: Ron Rise Date: Wed, 1 Nov 2023 17:03:15 -0400 Subject: [PATCH] =?UTF-8?q?I=20know,=20I=20know,=20this=20is=20not=20how?= =?UTF-8?q?=20I=E2=80=99m=20supposed=20to=20do=20it,=20but=20I=20can't=20t?= =?UTF-8?q?hink=20of=20something=20better.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + LICENSE | 21 ++ README.md | 69 +++++ composer.json | 24 ++ composer.lock | 346 +++++++++++++++++++++++++ src/Client.php | 283 ++++++++++++++++++++ src/Exceptions/ValidationException.php | 13 + src/Transports/ApiTransport.php | 249 ++++++++++++++++++ src/Transports/TransportInterface.php | 19 ++ src/Validator.php | 31 +++ 10 files changed, 1057 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 src/Client.php create mode 100644 src/Exceptions/ValidationException.php create mode 100644 src/Transports/ApiTransport.php create mode 100644 src/Transports/TransportInterface.php create mode 100644 src/Validator.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f33dd5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +vendor/ +.idea/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a190ed2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Siteworx Professionals, LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..98e882b --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +## Mail client for the siteworx pro email API + +https://email.siteworxpro.com/api + +Access to the API is restricted and requires an account. + +`composer require siteworx/mail-client` + +**Requires** PHP >7.0 + +Usage + +```php +require 'vendor/autoload.php'; + +$transport = new Siteworx\Mail\Transports\ApiTransport([ + 'client_id' => 'k4ndk...4kkfa', + 'client_secret' => 'Jdv4...4kvD' +]); + +$client = new Siteworx\Mail\Client($transport); + +$client->setSubject('Test Subject'); +$client->setFrom('from@email.com'); + +$client->addTo('an@email.com'); +$client->addTo('another@email.com'); + +$client->setBody('Test Message!'); + +$result = $client->send(); +``` + +You can provide a cache to the api transport and your api token will +automatically be cached for it's lifetime. + +```php + +$memcache = new Memcache; +$memcache->addServer($host); + +$transport = new Siteworx\Mail\Transports\ApiTransport([ + 'client_id' => 'k4ndk...4kkfa', + 'client_secret' => 'Jdv4...4kvD' +]); + +$transport->setCache($memcache); +``` + +You can use any cache that implements the PSR-6 CacheInterface. + +**Catching Message** + +You can catch message if you are testing by passing in the value of `true` to the send method +```php +$client->send(true); +``` +The payload will be sent to the api and validated but will be caught before it is sent. + +**Delaying Messages** + +You can delay message so they are sent at a specific time. + +```php +$time = new DateTime(); +$time->add(new DateInterval('P1D')); +$client->sendTime($time); +``` +This will send the email the next day. \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..331626a --- /dev/null +++ b/composer.json @@ -0,0 +1,24 @@ +{ + "name": "siteworx/mail-client", + "description": "api client for the siteworx professionals api", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Siteworx Pro", + "email": "websites@siteworxpro.com" + } + ], + "autoload": { + "psr-4": { + "Siteworx\\Mail\\": "src/" + } + }, + "minimum-stability": "dev", + "require": { + "php": ">=7.0.0", + "psr/log": "^1.0", + "psr/simple-cache": "^1.0", + "guzzlehttp/guzzle": "^6.3" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..638f35d --- /dev/null +++ b/composer.lock @@ -0,0 +1,346 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "content-hash": "e6242594ada0bd472c34ec3bbac0645e", + "packages": [ + { + "name": "guzzlehttp/guzzle", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f4db5a78a5ea468d4831de7f0bf9d9415e348699", + "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699", + "shasum": "" + }, + "require": { + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.4", + "php": ">=5.5" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.0 || ^5.0", + "psr/log": "^1.0" + }, + "suggest": { + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.2-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2017-06-22T18:50:49+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "09e549f5534380c68761260a71f847644d8f65aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/09e549f5534380c68761260a71f847644d8f65aa", + "reference": "09e549f5534380c68761260a71f847644d8f65aa", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "time": "2017-05-20T23:14:18+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "811b676fbab9c99e359885032e5ebc70e442f5b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/811b676fbab9c99e359885032e5ebc70e442f5b8", + "reference": "811b676fbab9c99e359885032e5ebc70e442f5b8", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "request", + "response", + "stream", + "uri", + "url" + ], + "time": "2017-07-17T09:11:21+00:00" + }, + { + "name": "psr/http-message", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "psr/log", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2016-10-10T12:19:37+00:00" + }, + { + "name": "psr/simple-cache", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "753fa598e8f3b9966c886fe13f370baa45ef0e24" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/753fa598e8f3b9966c886fe13f370baa45ef0e24", + "reference": "753fa598e8f3b9966c886fe13f370baa45ef0e24", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "time": "2017-01-02T13:31:39+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=7.0.0" + }, + "platform-dev": [] +} diff --git a/src/Client.php b/src/Client.php new file mode 100644 index 0000000..2a91ed8 --- /dev/null +++ b/src/Client.php @@ -0,0 +1,283 @@ +_transport = $transport; + } + + /** + * @param string $to + * @throws ValidationException + */ + public function addTo(string $to) + { + if (!Validator::validateEmailAddress($to)) { + throw new ValidationException('Email address is invalid'); + } + + $this->_to[] = $to; + + } + + /** + * @param array $to + * @throws ValidationException + */ + public function setAllTo(array $to) + { + foreach ($to as $item) { + if (!Validator::validateEmailAddress($item)) { + throw new ValidationException('Email address is invalid'); + } + } + + $this->_to = $to; + } + + /** + * @param string $cc + * @throws ValidationException + */ + public function addCc(string $cc) + { + if (!Validator::validateEmailAddress($cc)) { + throw new ValidationException('Email address is invalid'); + } + + $this->_cc[] = $cc; + } + + /** + * @param string $bcc + * @throws ValidationException + */ + public function addBcc(string $bcc) + { + if (!Validator::validateEmailAddress($bcc)) { + throw new ValidationException('Email address is invalid'); + } + + $this->_bcc[] = $bcc; + } + + /** + * @param string $body + * @param bool $isHtml + */ + public function setBody(string $body, bool $isHtml = false) + { + $this->_body = $body; + $this->_isHtml = $isHtml; + } + + /** + * @param string $subject + */ + public function setSubject(string $subject) + { + $this->_subject = $subject; + } + + /** + * @param string $from + * + * @throws ValidationException + */ + public function setFrom(string $from) + { + if (!Validator::validateEmailAddress($from)) { + throw new ValidationException('Email address is invalid'); + } + + $this->_from = $from; + } + + /** + * @param bool $catch + * @throws ValidationException + * @return mixed + */ + public function send(bool $catch = false) + { + $this->_catch = $catch; + $payload = $this->_buildPayload(); + return $this->_transport->sentMailPayload($payload); + } + + /** + * @param string $uuid + * @return bool + */ + public function delete(string $uuid): bool + { + return $this->_transport->deleteEmail($uuid); + } + + /** + * @throws ValidationException + * @return array + */ + private function _buildPayload(): array + { + + $this->_validateFields(); + + $mailPayload = [ + 'Destination' => [ + 'ToAddresses' => $this->_to + ], + 'Message' => [ + 'Subject' => [ + 'Data' => $this->_subject + ] + ], + 'Source' => $this->_from + ]; + + if (!empty($this->_cc)) { + $mailPayload['Destination']['CcAddresses'] = $this->_cc; + } + + if (!empty($this->_bcc)) { + $mailPayload['Destination']['BccAddresses'] = $this->_bcc; + } + + if ($this->_isHtml) { + $mailPayload['Message']['Body']['Html']['Data'] = $this->_body; + $mailPayload['Message']['Body']['Text']['Data'] = htmlentities($this->_body); + } else { + $mailPayload['Message']['Body']['Text']['Data'] = $this->_body; + } + + if ($this->_catch) { + $mailPayload['Catch'] = true; + } + + if ($this->_sendTime !== false) { + $mailPayload['ScheduledTime'] = $this->_sendTime->format('Y-m-d H:i:s'); + } + + if (\count($this->_files) > 0) { + $files = []; + + foreach ($this->_files as $file) { + $files[] = [ + 'FileName' => basename($file), + 'Content' => base64_encode(file_get_contents($file)) + ]; + } + + $mailPayload['Attachments'] = $files; + } + + return $mailPayload; + + } + + /** + * @throws ValidationException + */ + private function _validateFields() + { + if (empty($this->_to)) { + throw new ValidationException('To Address is required'); + } + + if (empty($this->_from)) { + throw new ValidationException('From Address is required'); + } + } + + /** + * @param string $fileLocation + * + * @throws ValidationException + */ + public function addAttachment(string $fileLocation) + { + if (!file_exists($fileLocation)) { + throw new ValidationException('File does not exist.'); + } + $this->_files[] = $fileLocation; + } + + /** + * @param \DateTimeInterface $sendTime + */ + public function sendTime(\DateTimeInterface $sendTime) + { + $this->_sendTime = $sendTime; + } +} \ No newline at end of file diff --git a/src/Exceptions/ValidationException.php b/src/Exceptions/ValidationException.php new file mode 100644 index 0000000..843d188 --- /dev/null +++ b/src/Exceptions/ValidationException.php @@ -0,0 +1,13 @@ +_client = $guzzle ?? new Guzzle(); + $this->_clientId = $config['client_id']; + $this->_clientSecret = $config['client_secret']; + + + } + + /** + * @param mixed $Cache + */ + public function setCache(CacheInterface $Cache) + { + $this->_cache = $Cache; + } + + /** + * @param LoggerInterface $logger + */ + public function setLogger(LoggerInterface $logger) + { + $this->_logger = $logger; + } + + /** + * @return string + */ + public function getAccessToken(): string + { + return $this->_accessToken; + } + + /** + * @param string $clientId + */ + public function setClientId(string $clientId) + { + $this->_clientId = $clientId; + } + + /** + * @param string $clientSecret + */ + public function setClientSecret(string $clientSecret) + { + $this->_clientSecret = $clientSecret; + } + + /** + * @param array $payload + * @return \stdClass + */ + public function sentMailPayload(array $payload) + { + + $this->setToken(); + + if ($this->_logger !== null) { + $this->_logger->info('Sending Email.'); + } + + try { + $result = $this->_client->post($this->_apiEndpoint . '/api/email', [ + 'form_params' => $payload, + 'headers' => [ + 'Authorization' => 'Bearer ' . $this->_accessToken + ] + ]); + + $body = $result->getBody()->getContents(); + $data = json_decode($body); + + if ($this->_logger !== null) { + $this->_logger->info('Success!'); + $this->_logger->debug(\json_encode($body)); + } + + } catch (ServerException $exception) { + + $result = $exception->getResponse(); + $body = $result->getBody()->getContents(); + + $data = json_decode($body); + + if ($this->_logger !== null) { + $this->_logger->warning('An error occurred sending the email! (' . $result->getStatusCode() . ')'); + $this->_logger->debug(\json_encode($body)); + } + + } catch (RequestException $exception) { + + $result = $exception->getResponse(); + $body = $result->getBody()->getContents(); + + $data = json_decode($body); + + if ($this->_logger !== null) { + $this->_logger->warning('An error occurred sending the email! (' . $result->getStatusCode() . ')'); + $this->_logger->debug(\json_encode($body)); + } + + } + + return $data; + + } + + private function setToken() + { + if ($this->_cache !== null) { + $this->_accessToken = $this->_cache->get('access_token'); + } else { + if ($this->_logger !== null) { + $this->_logger->notice('No cache available for client. Providing a cache interface can improve client response times.'); + } + } + + if (empty($this->_accessToken)) { + $this->refreshToken(); + } + + } + + /** + * @return \stdClass + */ + private function refreshToken() + { + $params = [ + 'scope' => 'default', + 'grant_type' => 'client_credentials', + 'client_id' => $this->_clientId, + 'client_secret' => $this->_clientSecret + ]; + try { + $result = $this->_client->post($this->_apiEndpoint . '/access_token', [ + 'form_params' => $params + ]); + + $body = $result->getBody()->getContents(); + $data = json_decode($body); + + $this->_accessToken = $data->access_token; + + if ($this->_cache !== null) { + $this->_cache->set('access_token', $this->_accessToken, $data->expires_in); + } + + } catch (ServerException $exception) { + + $result = $exception->getResponse(); + $body = $result->getBody()->getContents(); + + $data = json_decode($body); + + } catch (RequestException $exception) { + + $result = $exception->getResponse(); + $body = $result->getBody()->getContents(); + + $data = json_decode($body); + + } + + return $data; + + } + + public function deleteEmail(string $uuid): bool + { + $this->setToken(); + + try { + $response = $this->_client->delete($this->_apiEndpoint .'/api/email/' . $uuid, [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $this->_accessToken + ] + ]); + + return $response->getStatusCode() === 200; + } catch (RequestException $exception) { + return false; + } + } +} \ No newline at end of file diff --git a/src/Transports/TransportInterface.php b/src/Transports/TransportInterface.php new file mode 100644 index 0000000..36fd27e --- /dev/null +++ b/src/Transports/TransportInterface.php @@ -0,0 +1,19 @@ +^', $email, $matches); + + return \count($matches) > 0 ? str_replace(['<', '>'], '', $matches[0]) : $email; + } + +} \ No newline at end of file