Skip to content

Instantly share code, notes, and snippets.

@belambic
Created July 11, 2023 15:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save belambic/9cf9b6c37ed2ddaebe3abcbf8f6ea95b to your computer and use it in GitHub Desktop.
Save belambic/9cf9b6c37ed2ddaebe3abcbf8f6ea95b to your computer and use it in GitHub Desktop.
Sendgrid API
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