Created
July 11, 2023 15:42
-
-
Save belambic/9cf9b6c37ed2ddaebe3abcbf8f6ea95b to your computer and use it in GitHub Desktop.
Sendgrid API
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/sendgrid_integration.services.yml b/sendgrid_integration.services.yml | |
new file mode 100644 | |
index 0000000..f3eb647 | |
--- /dev/null | |
+++ b/sendgrid_integration.services.yml | |
@@ -0,0 +1,13 @@ | |
+services: | |
+ | |
+ sendgrid_integration.api: | |
+ class: Drupal\sendgrid_integration\Api | |
+ arguments: | |
+ - '@config.factory' | |
+ - '@messenger' | |
+ - '@logger.factory' | |
+ - '@module_handler' | |
+ - '@cache_factory' | |
+ tags: | |
+ - { name: 'sendgrid_integration' } | |
+ | |
diff --git a/src/Api.php b/src/Api.php | |
new file mode 100644 | |
index 0000000..7dfe3c2 | |
--- /dev/null | |
+++ b/src/Api.php | |
@@ -0,0 +1,623 @@ | |
+<?php | |
+ | |
+namespace Drupal\sendgrid_integration; | |
+ | |
+use Drupal\Component\Utility\Xss; | |
+use Drupal\Core\Config\ConfigFactoryInterface; | |
+use Drupal\Core\Extension\ModuleHandlerInterface; | |
+use Drupal\Core\Logger\LoggerChannelFactoryInterface; | |
+use Drupal\Core\Messenger\MessengerInterface; | |
+use Drupal\Core\Cache\CacheFactoryInterface; | |
+use GuzzleHttp\Client; | |
+use GuzzleHttp\Exception\ClientException; | |
+use \Exception; | |
+ | |
+/** | |
+ * Class SendGridReportsController. | |
+ * | |
+ * @package Drupal\sengrid_integration\Controller | |
+ */ | |
+class Api { | |
+ | |
+ /** | |
+ * Api Key of SendGrid. | |
+ * | |
+ * @var array|mixed|null | |
+ */ | |
+ protected $apiKey = NULL; | |
+ | |
+ /** | |
+ * Cache bin of SendGrid Reports module. | |
+ * | |
+ * @var string | |
+ */ | |
+ protected $bin = 'sendgrid_integration'; | |
+ | |
+ /** | |
+ * Include the messenger service. | |
+ * | |
+ * @var \Drupal\Core\Messenger\MessengerInterface | |
+ */ | |
+ protected $messenger; | |
+ | |
+ /** | |
+ * The config factory. | |
+ * | |
+ * @var \Drupal\Core\Config\ConfigFactoryInterface | |
+ */ | |
+ protected $configFactory; | |
+ | |
+ /** | |
+ * Logger service. | |
+ * | |
+ * @var \Drupal\Core\Logger\LoggerChannelFactory | |
+ */ | |
+ protected $loggerFactory; | |
+ | |
+ /** | |
+ * The module handler service. | |
+ * | |
+ * @var \Drupal\Core\Extension\ModuleHandlerInterface | |
+ */ | |
+ protected $moduleHandler; | |
+ | |
+ /** | |
+ * The cache factory service. | |
+ * | |
+ * @var \Drupal\Core\Cache\CacheFactoryInterface | |
+ */ | |
+ protected $cacheFactory; | |
+ | |
+ /** | |
+ * The subuser to perform API calls on behalf of. | |
+ */ | |
+ protected $subuser; | |
+ | |
+ /** | |
+ * Api constructor. | |
+ * | |
+ * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory | |
+ * The configuration factory. | |
+ * @param \Drupal\Core\Messenger\MessengerInterface $messenger | |
+ * The messenger service. | |
+ * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory | |
+ * The logger factory. | |
+ * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler | |
+ * The module handler service. | |
+ * @param \Drupal\Core\Cache\CacheFactoryInterface $cacheFactory | |
+ * The cache factory service. | |
+ */ | |
+ public function __construct(ConfigFactoryInterface $config_factory, MessengerInterface $messenger, LoggerChannelFactoryInterface $logger_factory, ModuleHandlerInterface $moduleHandler, CacheFactoryInterface $cacheFactory) { | |
+ $this->configFactory = $config_factory; | |
+ $this->messenger = $messenger; | |
+ $this->loggerFactory = $logger_factory; | |
+ $this->moduleHandler = $moduleHandler; | |
+ $this->cacheFactory = $cacheFactory; | |
+ | |
+ // Load key from variables and throw errors if not there. | |
+ $key_secret = $this->configFactory | |
+ ->get('sendgrid_integration.settings') | |
+ ->get('apikey'); | |
+ | |
+ if ($this->moduleHandler->moduleExists('key')) { | |
+ $key = \Drupal::service('key.repository')->getKey($key_secret); | |
+ if ($key && $key->getKeyValue()) { | |
+ $this->apiKey = $key->getKeyValue(); | |
+ } | |
+ } | |
+ else { | |
+ $this->apiKey = $key_secret; | |
+ } | |
+ | |
+ // Display message one time if api key is not set. | |
+ if (empty($this->apiKey)) { | |
+ $this->loggerFactory->get('sengrid_integration') | |
+ ->warning(t('SendGrid Module is not setup with API key.')); | |
+ $this->messenger->addWarning('Sendgrid Module is not setup with an API key.'); | |
+ } | |
+ } | |
+ | |
+ /** | |
+ * Set the subuser to perform API calls on behalf of. | |
+ * | |
+ * @param string $subuser | |
+ * A valid subuser. | |
+ * | |
+ * @return \Drupal\sendgrid_integration\Api | |
+ * This object. | |
+ */ | |
+ public function setSubuser(string $subuser):Api { | |
+ $this->subuser = $subuser; | |
+ return $this; | |
+ } | |
+ | |
+ /** | |
+ * Sets the cache to sengrid_integration bin. | |
+ * | |
+ * @param string $cid | |
+ * Cache Id. | |
+ * @param array $data | |
+ * The data should be cached. | |
+ */ | |
+ protected function setCache($cid, array $data) { | |
+ if (!empty($data)) { | |
+ $this->cacheFactory->get($this->bin)->set($cid, $data); | |
+ } | |
+ } | |
+ | |
+ /** | |
+ * Get the guzzle client. | |
+ * | |
+ * @param bool $parent | |
+ * True if we want to use the parent user for this request. | |
+ * | |
+ * @return \Guzzlehttp\Client | |
+ * The Guzzle client object. | |
+ */ | |
+ protected function getClient($parent = FALSE) { | |
+ $headers['Authorization'] = 'Bearer ' . $this->apiKey; | |
+ if ($this->subuser && !$parent) { | |
+ $headers['on-behalf-of'] = $this->subuser; | |
+ } | |
+ $client = new Client([ | |
+ 'base_uri' => 'https://api.sendgrid.com/v3/', | |
+ 'headers' => $headers, | |
+ ]); | |
+ return $client; | |
+ } | |
+ | |
+ /** | |
+ * Get request to SendGrid. | |
+ * | |
+ * @param string $path | |
+ * Part of SendGrid endpoint. | |
+ * @param array $query | |
+ * Query params to the request. | |
+ * @param bool $parent | |
+ * Perform the request as the parent user. | |
+ * | |
+ * @return bool|mixed | |
+ * Decoded json or FALSE. | |
+ */ | |
+ protected function get($path, array $query = [], $parent = FALSE) { | |
+ $client = $this->getClient($parent); | |
+ | |
+ // Lets attempt the request and catch an error if it fails. | |
+ try { | |
+ $response = $client->get($path, ['query' => $query]); | |
+ } | |
+ catch (ClientException $e) { | |
+ $code = Xss::filter($e->getCode()); | |
+ $this->loggerFactory->get('sengrid_integration') | |
+ ->error(t('SendGrid module failed to receive data. HTTP Error Code @errno', ['@errno' => $code])); | |
+ $this->messenger->addError(t('SendGrid module failed to receive data. See logs.')); | |
+ return FALSE; | |
+ } | |
+ // Sanitize return before using in Drupal. | |
+ $body = Xss::filter($response->getBody()); | |
+ return json_decode($body); | |
+ } | |
+ | |
+ /** | |
+ * Post request to SendGrid. | |
+ * | |
+ * @param string $path | |
+ * Part of SendGrid endpoint. | |
+ * @param array $data | |
+ * Query params to the request. | |
+ * @param bool $parent | |
+ * Perform the request as the parent user. | |
+ * | |
+ * @return bool|mixed | |
+ * Decoded json or FALSE. | |
+ */ | |
+ protected function post($path, array $data, $parent = FALSE) { | |
+ $client = $this->getClient($parent); | |
+ | |
+ // Lets attempt the request and catch an error if it fails. | |
+ try { | |
+ $response = $client->post($path, ['json' => $data]); | |
+ } | |
+ catch (ClientException $e) { | |
+ $code = Xss::filter($e->getCode()); | |
+ $this->loggerFactory->get('sengrid_integration') | |
+ ->error(t('SendGrid module failed to post data. HTTP Error Code @errno', ['@errno' => $code])); | |
+ $this->messenger->addError(t('SendGrid module failed to post data. See logs.')); | |
+ return FALSE; | |
+ } | |
+ // Sanitize return before using in Drupal. | |
+ $body = Xss::filter($response->getBody()); | |
+ return json_decode($body); | |
+ } | |
+ | |
+ /** | |
+ * Delete request to SendGrid. | |
+ * | |
+ * @param string $path | |
+ * Part of SendGrid endpoint. | |
+ * @param string $data | |
+ * The id of the item to be deleted. | |
+ * @param bool $parent | |
+ * Perform the request as the parent user. | |
+ * | |
+ * @return bool|mixed | |
+ * Decoded json or FALSE. | |
+ */ | |
+ protected function delete($path, string $data, $parent = FALSE) { | |
+ $client = $this->getClient($parent); | |
+ // Lets attempt the request and catch an error if it fails. | |
+ try { | |
+ $response = $client->delete($path . '/' . $data); | |
+ } | |
+ catch (ClientException $e) { | |
+ $code = Xss::filter($e->getCode()); | |
+ $this->loggerFactory->get('sengrid_integration') | |
+ ->error(t('SendGrid module failed to receive data. HTTP Error Code @errno', ['@errno' => $code])); | |
+ $this->messenger->addError(t('SendGrid module failed to receive data. See logs.')); | |
+ return FALSE; | |
+ } | |
+ // Sanitize return before using in Drupal. | |
+ $body = Xss::filter($response->getBody()); | |
+ return json_decode($body); | |
+ } | |
+ | |
+ /** | |
+ * Get subuser info for the passed username. | |
+ * | |
+ * @param string $username | |
+ * A string to search usernames for. | |
+ * | |
+ * @return array | |
+ * Data relating to the subuser. | |
+ */ | |
+ public function getSubUser(string $username): array { | |
+ $data = []; | |
+ $data['username'] = $username; | |
+ $response = $this->get('subusers', $data); | |
+ return $response; | |
+ } | |
+ | |
+ /** | |
+ * Create a subuser. | |
+ * | |
+ * @param string $username | |
+ * The username of the subuser being created. | |
+ * @param string $email | |
+ * A valid email address for the subuser. | |
+ * @param string $password | |
+ * The subuser password. | |
+ * @param array $ips | |
+ * An array of IP addresses to associated with the user. | |
+ * If this is not passed, the least currently used IP will be used. | |
+ * | |
+ * @return array | |
+ * Response from sendgrid. | |
+ */ | |
+ public function createSubUser(string $username, string $email, string $password, array $ips = []): array { | |
+ $existing = $this->getSubUser($username); | |
+ foreach ($existing as $subuser) { | |
+ if ($subuser->username == $username) { | |
+ // @todo use custom exception. | |
+ throw new Exception('Username already exists: ' . $username); | |
+ } | |
+ } | |
+ if (!$ips) { | |
+ $ips = [$this->getLeastUsedIp()]; | |
+ } | |
+ $data = [ | |
+ 'username' => $username, | |
+ 'email' => $email, | |
+ 'password' => $password, | |
+ 'ips' => $ips, | |
+ ]; | |
+ $response = $this->post('subusers', $data, TRUE); | |
+ // Response from sendgrid doesn't give the IPs, so add it here. | |
+ if (is_object($response)) { | |
+ $response->ips = $ips; | |
+ } | |
+ return (array) $response; | |
+ } | |
+ | |
+ /** | |
+ * Delete a subuser. | |
+ * | |
+ * @param string $username | |
+ * The username of the subuser being deleted. | |
+ */ | |
+ public function deleteSubuser(string $username) { | |
+ $existing = $this->getSubUser($username); | |
+ $response = FALSE; | |
+ foreach ($existing as $subuser) { | |
+ if ($subuser->username == $username) { | |
+ $response = $this->delete('subusers', $subuser->username); | |
+ } | |
+ } | |
+ if ($response === FALSE) { | |
+ throw new Exception('Subuser not found.'); | |
+ } | |
+ } | |
+ | |
+ /** | |
+ * Create an API key. | |
+ * | |
+ * @param $name | |
+ * A name for the API key. | |
+ * @param $perms | |
+ * The perms to assign to this API key. NULL means full access. | |
+ * | |
+ * @return array | |
+ * Response from sendgrid. | |
+ */ | |
+ public function createApiKey($name, $perms = NULL): array { | |
+ if (!$this->subuser) { | |
+ // @todo use a custom exception. | |
+ throw new Exception('Attempt to create an API key without a subuser set.'); | |
+ } | |
+ if (!$perms) { | |
+ $perms = $this->fullAccess(); | |
+ } | |
+ $data = [ | |
+ 'name' => $name, | |
+ 'scopes' => $perms, | |
+ ]; | |
+ return (array) $this->post('api_keys', $data); | |
+ } | |
+ | |
+ /** | |
+ * Get a list of valid IP Addresses. | |
+ * | |
+ * @param array $mapping | |
+ * An optional array of mappings to use to filter the ip list. | |
+ * | |
+ * @return array | |
+ * Array of valid IP addresses. | |
+ */ | |
+ public function getIpAddresses(array $mappings = []): array { | |
+ $ips = $this->get('ips'); | |
+ if (!empty($mapping)) { | |
+ foreach ($ips as $key => $ip) { | |
+ if (!isset($mapping[$ip->ip])) { | |
+ unset($ips[$key]); | |
+ } | |
+ } | |
+ } | |
+ return $ips; | |
+ } | |
+ | |
+ /** | |
+ * Get the IP address with the least number of associated subusers. | |
+ * | |
+ * @param array $mapping | |
+ * An optional array of mappings to use to filter the ip list. | |
+ * | |
+ * @return string | |
+ * The IP address with the least associated subusers. | |
+ */ | |
+ public function getLeastUsedIp(array $mappings = []): string { | |
+ $ips = $this->getIpAddresses($mappings); | |
+ $least = $this->least($ips, 'ip'); | |
+ return $least; | |
+ } | |
+ | |
+ /** | |
+ * Create a new authenticated domain record. | |
+ * | |
+ * @param string $domain | |
+ * The domain to authenticate. | |
+ */ | |
+ public function createDomain(string $domain): array { | |
+ $data = ['domain' => $domain]; | |
+ $response = $this->post('whitelabel/domains', $data, TRUE); | |
+ return (array) $response; | |
+ } | |
+ | |
+ /** | |
+ * Verify a domain record. | |
+ * | |
+ * @param int $id | |
+ * The id of the domain record. | |
+ */ | |
+ public function verifyDomain(int $id): array { | |
+ $response = $this->post('whitelabel/domains/' . $id . '/validate', [], TRUE); | |
+ return (array) $response; | |
+ } | |
+ | |
+ /** | |
+ * Get a domain id from a domain name. | |
+ * | |
+ * @param string $domain | |
+ * The domain to authenticate. | |
+ */ | |
+ public function getDomainId(string $domain): int { | |
+ $domains = $this->getDomains(); | |
+ $id = 0; | |
+ foreach ($domains as $candidate) { | |
+ if ($candidate->domain == $domain) { | |
+ $id = $candidate->id; | |
+ break; | |
+ } | |
+ } | |
+ return $id; | |
+ } | |
+ | |
+ /** | |
+ * Get a list of available domains. | |
+ * | |
+ * @return array | |
+ * Array of valid send domains. | |
+ */ | |
+ public function getDomains() { | |
+ $domains = $this->get('whitelabel/domains', [], TRUE); | |
+ return $domains; | |
+ } | |
+ | |
+ /** | |
+ * Get the domain with the least number of associated subusers. | |
+ * | |
+ * @return string | |
+ * The domain with the least associated subusers. | |
+ */ | |
+ public function getLeastUsedDomain() { | |
+ $domains = $this->getDomains(); | |
+ $least = $this->least($domains, 'id'); | |
+ return $least; | |
+ } | |
+ | |
+ /** | |
+ * Associated a domain with a subuser. | |
+ * | |
+ * @param $domain | |
+ * Domain to associate. Least used domain will be picked if this is empty. | |
+ */ | |
+ public function associateDomain($domain = '') { | |
+ if (!$this->subuser) { | |
+ throw new Exception('Attempt to associated a domain without a subuser set.'); | |
+ } | |
+ $domain = $domain ?? $this->getLeastUsedDomain(); | |
+ $data = ['username' => $this->subuser]; | |
+ $response = $this->post('whitelabel/domains/' . $domain . '/subuser', $data, TRUE); | |
+ return $response; | |
+ } | |
+ | |
+ /** | |
+ * Get a list of available link brandings. | |
+ * | |
+ * @return array | |
+ * Array of available branding links. | |
+ */ | |
+ public function getLinks() { | |
+ $links = $this->get('whitelabel/links'); | |
+ return $links; | |
+ } | |
+ | |
+ /** | |
+ * Get the link with the least number of associated subusers. | |
+ * | |
+ * @return string | |
+ * Link with the least associated subusers. | |
+ */ | |
+ public function getLeastUsedLink() { | |
+ $links = $this->getLinks(); | |
+ $least = $this->least($links, 'id'); | |
+ return $least; | |
+ } | |
+ | |
+ /** | |
+ * Associated a link with a subuser. | |
+ */ | |
+ public function associateLink($link) { | |
+ if (!$this->subuser) { | |
+ throw new Exception('Attempt to associated a link without a subuser set.'); | |
+ } | |
+ $link = $link ?? $this->getLeastUsedLink(); | |
+ $data = ['username' => $this->subuser]; | |
+ $response = $this->post('whitelabel/links/' . $link . '/subuser', $data, TRUE); | |
+ return $response; | |
+ } | |
+ | |
+ /** | |
+ * Create a webhook. | |
+ */ | |
+ public function createWebhook($data) { | |
+ $response = $this->post('user/webhooks/event/settings', $data); | |
+ } | |
+ | |
+ /** | |
+ * Helper function to find the least number of subusers in an array. | |
+ * | |
+ * @param array $list | |
+ * A list of objects as returned from the Sendgrid API. | |
+ * @param string $return | |
+ * The name of the objectproperty to return. | |
+ * | |
+ * @return string | |
+ * The value of $return for the least associated subusers. | |
+ */ | |
+ private function least($list, $return) { | |
+ $prev = 0; | |
+ $least = ''; | |
+ foreach ($list as $row) { | |
+ $count = count($row->subusers); | |
+ if (!$prev || $count < $prev) { | |
+ $prev = $count; | |
+ $least = $row->$return; | |
+ } | |
+ } | |
+ return $least; | |
+ } | |
+ | |
+ /** | |
+ * Helper functions for api key scopes. | |
+ */ | |
+ private function fullAccess() { | |
+ return [ | |
+ "alerts.create", | |
+ "alerts.read", | |
+ "alerts.update", | |
+ "alerts.delete", | |
+ "ips.warmup.create", | |
+ "ips.warmup.read", | |
+ "ips.warmup.update", | |
+ "ips.warmup.delete", | |
+ "ips.pools.create", | |
+ "ips.pools.read", | |
+ "ips.pools.update", | |
+ "ips.pools.delete", | |
+ "ips.pools.ips.create", | |
+ "ips.pools.ips.read", | |
+ "ips.pools.ips.update", | |
+ "ips.pools.ips.delete", | |
+ "ips.read", | |
+ "mail.send", | |
+ "mail_settings.bcc.read", | |
+ "mail_settings.address_whitelist.read", | |
+ "mail_settings.footer.read", | |
+ "mail_settings.forward_spam.read", | |
+ "mail_settings.plain_content.read", | |
+ "mail_settings.spam_check.read", | |
+ "mail_settings.bounce_purge.read", | |
+ "mail_settings.forward_bounce.read", | |
+ "tracking_settings.click.read", | |
+ "tracking_settings.subscription.read", | |
+ "tracking_settings.open.read", | |
+ "tracking_settings.google_analytics.read", | |
+ "stats.read", | |
+ "stats.global.read", | |
+ "categories.stats.read", | |
+ "categories.stats.sums.read", | |
+"devices.stats.read", | |
+ "clients.stats.read", | |
+ "clients.phone.stats.read", | |
+ "clients.tablet.stats.read", | |
+ "clients.webmail.stats.read", | |
+ "clients.desktop.stats.read", | |
+ "geo.stats.read", | |
+ "mailbox_providers.stats.read", | |
+ "browsers.stats.read", | |
+ "user.webhooks.parse.stats.read", | |
+ "api_keys.read", | |
+ "categories.create", | |
+ "categories.read", | |
+ "categories.update", | |
+ "categories.delete", | |
+ "mail.batch.create", | |
+ "mail.batch.read", | |
+ "mail.batch.update", | |
+ "mail.batch.delete", | |
+ "access_settings.whitelist.create", | |
+ "access_settings.whitelist.read", | |
+ "access_settings.whitelist.update", | |
+ "access_settings.whitelist.delete", | |
+ "access_settings.activity.read", | |
+ "whitelabel.create", | |
+ "whitelabel.read", | |
+ "whitelabel.update", | |
+ "whitelabel.delete", | |
+ "suppression.create", | |
+ "suppression.read", | |
+ "suppression.update", | |
+ "suppression.delete", | |
+ ]; | |
+ } | |
+ | |
+} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment