Skip to content
Snippets Groups Projects

Issue #3224250: Subuser management

Open lambic requested to merge issue/sendgrid_integration-3224250:8.x-2.x into 8.x-2.x
2 files
+ 590
0
Compare changes
  • Side-by-side
  • Inline
Files
2
src/Api.php 0 → 100644
+ 577
0
 
<?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());
 
devel_debug($code);
 
$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;
 
}
 
devel_debug($response);
 
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->user) {
 
// @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 $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;
 
}
 
 
/**
 
* Get a list of available domains.
 
*
 
* @return array
 
* Array of valid send domains.
 
*/
 
public function getDomains() {
 
$domains = $this->get('whitelabel/domains', NULL, 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->user) {
 
throw new Exception('Attempt to associated a domain without a subuser set.');
 
}
 
$domain = $domain ?? $this->getLeastUsedDomain();
 
$data = ['username' => $this->user];
 
$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->user) {
 
throw new Exception('Attempt to associated a link without a subuser set.');
 
}
 
$link = $link ?? $this->getLeastUsedLink();
 
$data = ['username' => $this->user];
 
$response = $this->post('whitelabel/links/' . $link . '/subuser', $data, TRUE);
 
return $response;
 
}
 
 
/**
 
* 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",
 
];
 
}
 
 
}
Loading