PMCORE-3929 MS Modern Authentication / GMAIL API IMAP for ABE

This commit is contained in:
Julio Cesar Laura Avendaño
2022-08-18 19:30:28 +00:00
parent 4189165ce5
commit 53375b02e3
10 changed files with 1845 additions and 633 deletions

View File

@@ -65,7 +65,8 @@
"aws/aws-sdk-php": "~3.0",
"cretueusebiu/laravel-javascript": "^0.2.1",
"stevenmaguire/oauth2-microsoft": "^2.2",
"phpseclib/mcrypt_compat": "^2.0"
"phpseclib/mcrypt_compat": "^2.0",
"microsoft/microsoft-graph": "^1.7"
},
"require-dev": {
"guzzlehttp/guzzle": "^6.3",
@@ -128,5 +129,11 @@
"bootstrap/classaliasmap.php"
]
}
},
"config": {
"allow-plugins": {
"kylekatarnls/update-helper": true,
"typo3/class-alias-loader": true
}
}
}

2032
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@ use Illuminate\Support\Facades\Log;
use PHPMailer\PHPMailer\OAuth;
use PHPMailer\PHPMailer\PHPMailer;
use ProcessMaker\Core\System;
use ProcessMaker\Office365OAuth\Office365OAuth;
/**
* @package workflow.engine.ProcessMaker
@@ -565,6 +566,12 @@ class SpoolRun
$phpMailer->Username = $this->config['MESS_ACCOUNT'];
$phpMailer->Password = $this->config['MESS_PASSWORD'];
} else {
// Define initial options for the provider
$options = [
'clientId' => $this->config['OAUTH_CLIENT_ID'],
'clientSecret' => $this->config['OAUTH_CLIENT_SECRET'],
'accessType' => 'offline'
];
// Get provider
switch ($this->config['MESS_ENGINE']) {
case 'GMAILAPI':
@@ -572,18 +579,14 @@ class SpoolRun
break;
case 'OFFICE365API':
$providerClass = '\Stevenmaguire\OAuth2\Client\Provider\Microsoft';
$options['urlAuthorize'] = Office365OAuth::URL_AUTHORIZE;
$options['urlAccessToken'] = Office365OAuth::URL_ACCESS_TOKEN;
break;
default:
throw new Exception('Only Google and Microsoft OAuth2 providers are currently supported.');
break;
}
$provider = new $providerClass(
[
'clientId' => $this->config['OAUTH_CLIENT_ID'],
'clientSecret' => $this->config['OAUTH_CLIENT_SECRET'],
'accessType' => 'offline'
]
);
$provider = new $providerClass($options);
// Set OAuth to use
$phpMailer->setOAuth(

View File

@@ -20,11 +20,10 @@ try {
$office365Client = $office365OAuth->getOffice365Client();
$accessToken = $office365Client->getAccessToken('authorization_code', [
'code' => $_GET['code']
'code' => $_GET['code'],
'scope' => Office365OAuth::SMTP_SCOPE
]);
$token = $accessToken->getToken();
$office365OAuth->setRefreshToken($accessToken->getRefreshToken());
$office365OAuth->saveEmailServer();

View File

@@ -0,0 +1,190 @@
<?php
namespace ProcessMaker\BusinessModel\ActionsByEmail;
use Exception;
use Google_Client;
use Google_Service_Gmail;
use Google_Service_Gmail_ModifyMessageRequest;
use stdClass;
/**
* Class GmailMailbox
* @package ProcessMaker\BusinessModel\ActionsByEmail
*/
class GmailMailbox
{
private $service;
/**
* GmailMailbox constructor.
* @param array $emailSetup
*/
public function __construct(array $emailSetup)
{
// Google Client instance
$googleClient = new Google_Client();
$googleClient->setClientId($emailSetup['OAUTH_CLIENT_ID']);
$googleClient->setClientSecret($emailSetup['OAUTH_CLIENT_SECRET']);
$googleClient->refreshToken($emailSetup['OAUTH_REFRESH_TOKEN']);
$googleClient->setAccessType('offline');
$googleClient->addScope(Google_Service_Gmail::MAIL_GOOGLE_COM);
// Set Gmail service instance
$this->service = new Google_Service_Gmail($googleClient);
}
/**
* This function uses Gmail API to perform a search on the mailbox.
*
* @param string $criteria
* @return array
* @throws Exception
*/
public function searchMailbox(string $criteria = 'ALL'): array
{
// Transform criteria to values accepted by Gmail service
switch ($criteria) {
case 'UNSEEN':
$criteria = 'is:unread';
break;
case 'SEEN':
$criteria = 'is:read';
break;
default:
$criteria = '';
}
// Initialize variables
$nextPageToken = null;
$mailsIds = [];
// Get unread user's messages
try {
do {
// Build optional parameters array
$optParams = [
'q' => $criteria,
'pageToken' => $nextPageToken
];
// Get service response
$response = $this->service->users_messages->listUsersMessages('me', $optParams);
// Get the mails identifiers
$messages = $response->getMessages();
foreach ($messages as $message) {
$mailsIds[] = $message->getId();
}
// Get next page token
$nextPageToken = $response->getNextPageToken();
} while (!is_null($nextPageToken));
} catch (Exception $e) {
throw $e;
}
return $mailsIds;
}
/**
* Get mail data.
*
* @param string $mailId ID of the mail
* @param bool $markAsSeen Mark the email as seen, maintained by compatibility reasons, currently not used
* @return object
* @throws Exception
*/
public function getMail(string $mailId, bool $markAsSeen = true): object
{
try {
// Get message data
$response = $this->service->users_messages->get('me', $mailId);
// Get payload
$payload = $response->getPayload();
// Get headers
$headers = [];
foreach ($payload['headers'] as $item) {
$headers[$item->name] = $item->value;
}
// Get complete and decoded message body
$body = $this->getMessageBodyRecursive($payload);
} catch (Exception $e) {
throw $e;
}
// Build message object
$message = new stdClass();
$message->fromAddress = $headers['From'];
$message->toString = $headers['To'];
$message->subject = $headers['Subject'];
$message->textPlain = $body['plain'] ?? $body['html'];
return $message;
}
/**
* Remove UNREAD label in the mail.
*
* @param string $mailId
* @return void
* @throws Exception
*/
public function markMailAsRead($mailId): void
{
// Build modify message request
$modifyMessageRequest = new Google_Service_Gmail_ModifyMessageRequest();
$modifyMessageRequest->setRemoveLabelIds(['UNREAD']);
// Modify the mail
try {
$this->service->users_messages->modify('me', $mailId, $modifyMessageRequest);
} catch (Exception $e) {
throw $e;
}
}
/**
* Get message html body and plain body
*
* @param object $part
* @return array
*/
private function getMessageBodyRecursive(object $part): array
{
if ($part->mimeType == 'text/html') {
return [
'html' => $this->base64UrlDecode($part->body->data)
];
} else if ($part->mimeType == 'text/plain') {
return [
'plain' => $this->base64UrlDecode($part->body->data)
];
} else if ($part->parts) {
$return = [];
foreach ($part->parts as $subPart) {
$result = $this->getMessageBodyRecursive($subPart);
$return = array_merge($return, $result);
if (array_key_exists('html', $return)) {
break;
}
}
return $return;
}
return [];
}
/**
* Returns a base64 decoded web safe string
*
* @param string $string
* @return string
*/
private function base64UrlDecode(string $string): string
{
return base64_decode(str_replace(['-', '_'], ['+', '/'], $string));
}
}

View File

@@ -0,0 +1,149 @@
<?php
namespace ProcessMaker\BusinessModel\ActionsByEmail;
use Exception;
use League\OAuth2\Client\Grant\RefreshToken;
use Microsoft\Graph\Graph;
use Microsoft\Graph\Model;
use ProcessMaker\Office365OAuth\Office365OAuth;
use stdClass;
/**
* Class Office365Mailbox
* @package ProcessMaker\BusinessModel\ActionsByEmail
*/
class Office365Mailbox
{
const SCOPE = 'https://graph.microsoft.com/Mail.ReadWrite';
private $service;
private $messages = [];
/**
* Office365Mailbox constructor.
* @param array $emailSetup
* @throws Exception
*/
public function __construct(array $emailSetup)
{
// Get client instance
$office365OAuth = new Office365OAuth();
$office365OAuth->setClientID($emailSetup['OAUTH_CLIENT_ID']);
$office365OAuth->setClientSecret($emailSetup['OAUTH_CLIENT_SECRET']);
$provider = $office365OAuth->getOffice365Client();
// Get fresh access token
try {
$accessToken = $provider->getAccessToken(
new RefreshToken(),
[
'refresh_token' => $emailSetup['OAUTH_REFRESH_TOKEN'],
'scope' => self::SCOPE
]
);
} catch (Exception $e) {
throw $e;
}
// Set Office365 service instance
$this->service = new Graph();
$this->service->setAccessToken($accessToken->getToken());
}
/**
* This function uses Office365 API to perform a search on the mailbox.
*
* @param string $criteria
* @return array
* @throws Exception
*/
public function searchMailbox(string $criteria = 'ALL'): array
{
// Transform criteria to values accepted by Office365 service
switch ($criteria) {
case 'UNSEEN':
$criteria = 'isRead eq false';
break;
case 'SEEN':
$criteria = 'isRead eq true';
break;
default:
$criteria = '';
}
// Initialize variables
$nextLink = '';
$mailsIds = [];
// Get unread user's messages
try {
do {
// First time the link is generated, by default 100 results per call
if (empty($nextLink)) {
$nextLink = '/me/messages?$filter=' . $criteria . '&$select=id,body,toRecipients,from,subject&$top=100';
}
// Get service response
$response = $this->service->createRequest('GET', $nextLink)->execute();
// Get the mails identifiers
$messages = $response->getResponseAsObject(Model\Message::class);
foreach ($messages as $message) {
// Collect the messages identifiers
$mailsIds[] = $message->getId();
// Create a simple message object
$simpleMessage= new stdClass();
$simpleMessage->textPlain = strip_tags($message->getBody()->getContent());
$simpleMessage->toString = $message->getToRecipients()[0]['emailAddress']['address'];
$simpleMessage->fromAddress = $message->getFrom()->getEmailAddress()->getAddress();
$simpleMessage->subject = $message->getSubject();
// Add the new message object to messages array
$this->messages[$message->getId()] = $simpleMessage;
}
// Get next link
$nextLink = $response->getNextLink();
} while (!is_null($nextLink));
} catch (Exception $e) {
throw $e;
}
return $mailsIds;
}
/**
* Get mail data.
*
* @param string $mailId ID of the mail
* @param bool $markAsSeen Mark the email as seen, maintained by compatibility reasons, currently not used
* @return object
*/
public function getMail(string $mailId, bool $markAsSeen = true): object
{
return $this->messages[$mailId] ?? null;
}
/**
* Set "Is Read" property to "true" in the mail.
*
* @param string $mailId
* @return void
* @throws Exception
*/
public function markMailAsRead($mailId): void
{
// Set "Is Read" property to "true"
$message = new Model\Message();
$message->setIsRead(true);
// Get service response
try {
$this->service->createRequest('PATCH', '/me/messages/' . $mailId)->attachBody($message)->execute();
} catch (Exception $e) {
throw $e;
}
}
}

View File

@@ -133,18 +133,35 @@ class ResponseReader
public function getAllEmails(array $dataAbe)
{
try {
// Get Email Server info
$emailServer = new EmailServer();
$emailSetup = (!is_null(EmailServerPeer::retrieveByPK($dataAbe['ABE_EMAIL_SERVER_RECEIVER_UID']))) ?
$emailServer->getEmailServer($dataAbe['ABE_EMAIL_SERVER_RECEIVER_UID'], true) :
$emailServer->getEmailServerDefault();
if (empty($emailSetup) || (empty($emailSetup['MESS_INCOMING_SERVER']) && $emailSetup['MESS_INCOMING_PORT'] == 0)) {
throw (new Exception(G::LoadTranslation('ID_ABE_LOG_CANNOT_READ'), 500));
// Create an instance according to the engine type of the email server
if ($emailSetup['MESS_ENGINE'] === 'IMAP') {
if (empty($emailSetup['MESS_INCOMING_SERVER']) && $emailSetup['MESS_INCOMING_PORT'] == 0) {
throw new Exception(G::LoadTranslation('ID_ABE_LOG_CANNOT_READ'), 500);
}
$mailbox = new Mailbox(
'{'. $emailSetup['MESS_INCOMING_SERVER'] . ':' . $emailSetup['MESS_INCOMING_PORT'] . '/imap/ssl/novalidate-cert}INBOX',
$emailSetup['MESS_ACCOUNT'],
$this->decryptPassword($emailSetup)
);
} else {
if (empty($emailSetup['OAUTH_CLIENT_ID']) || empty($emailSetup['OAUTH_CLIENT_SECRET']) || empty($emailSetup['OAUTH_REFRESH_TOKEN'])) {
throw new Exception(G::LoadTranslation('ID_ABE_LOG_CANNOT_READ'), 500);
}
if ($emailSetup['MESS_ENGINE'] === 'GMAILAPI') {
$mailbox = new GmailMailbox($emailSetup);
} else if ($emailSetup['MESS_ENGINE'] === 'OFFICE365API') {
$mailbox = new Office365Mailbox($emailSetup);
}
}
Log::channel(':' . $this->channel)->debug("Open mailbox", Bootstrap::context($emailSetup));
// Read all messages into an array
@@ -152,7 +169,6 @@ class ResponseReader
if ($mailsIds) {
// Get the first message and save its attachment(s) to disk:
foreach ($mailsIds as $key => $mailId) {
/** @var IncomingMail $mail */
$mail = $mailbox->getMail($mailId, false);
Log::channel(':' . $this->channel)->debug("Get mail", Bootstrap::context(['mailId' => $mailId]));
if (!empty($mail->textPlain)) {
@@ -260,11 +276,11 @@ class ResponseReader
/**
* Derivation of the case with the mail information
* @param array $caseInfo
* @param IncomingMail $mail
* @param object $mail
* @param array $dataAbe
* @throws Exception
*/
public function processABE(array $caseInfo, IncomingMail $mail, array $dataAbe = [])
public function processABE(array $caseInfo, object $mail, array $dataAbe = [])
{
try {
$actionsByEmail = new ActionsByEmail();
@@ -383,11 +399,11 @@ class ResponseReader
* Send an error message to the sender
* @param string $msgError
* @param array $caseInf
* @param IncomingMail $mail
* @param object $mail
* @param array $emailSetup
* @return \ProcessMaker\Util\Response|string|\WsResponse
*/
public function sendMessageError($msgError, array $caseInf, IncomingMail $mail, array $emailSetup)
public function sendMessageError($msgError, array $caseInf, object $mail, array $emailSetup)
{
$wsBase = new WsBase();
$result = $wsBase->sendMessage(

View File

@@ -379,13 +379,17 @@ trait EmailBase
*/
public function sendTestMailWithPHPMailerOAuth($provider = 'League\OAuth2\Client\Provider\Google'): PHPMailerOAuth
{
$phpMailerOAuth = new PHPMailerOAuth([
'provider' => new $provider([
$options = [
'clientId' => $this->clientID,
'clientSecret' => $this->clientSecret,
'redirectUri' => $this->refreshToken,
'accessType' => 'offline'
]),
];
if ($provider === 'Stevenmaguire\OAuth2\Client\Provider\Microsoft') {
$options['urlAuthorize'] = self::URL_AUTHORIZE;
$options['urlAccessToken'] = self::URL_ACCESS_TOKEN;
}
$phpMailerOAuth = new PHPMailerOAuth([
'provider' => new $provider($options),
'clientId' => $this->clientID,
'clientSecret' => $this->clientSecret,
'refreshToken' => $this->refreshToken,

View File

@@ -11,11 +11,16 @@ class Office365OAuth
{
use EmailBase;
const URL_AUTHORIZE = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize';
const URL_ACCESS_TOKEN = 'https://login.microsoftonline.com/common/oauth2/v2.0/token';
const SMTP_SCOPE = 'https://outlook.office.com/SMTP.Send';
private $options = [
'scope' => [
'wl.imap',
'wl.offline_access'
]
'response_mode' => 'query',
'prompt' => 'consent',
// Scopes requested in authentication
'scope' => 'offline_access https://outlook.office.com/SMTP.Send https://graph.microsoft.com/Mail.ReadWrite'
];
/**
@@ -46,8 +51,11 @@ class Office365OAuth
'clientId' => $this->getClientID(),
'clientSecret' => $this->getClientSecret(),
'redirectUri' => $this->getRedirectURI(),
'urlAuthorize' => self::URL_AUTHORIZE,
'urlAccessToken' => self::URL_ACCESS_TOKEN,
'accessType' => 'offline'
]);
$provider->defaultScopes = $this->options['scope'];
return $provider;
}
}

View File

@@ -635,8 +635,8 @@ emailServer.application = {
["IMAP", "SMTP - IMAP (PHPMailer)"],
/*----------------------------------********---------------------------------*/
["MAIL", "Mail (PHP)"],
["GMAILAPI", "GMAIL API (PHPMailer)"],
["OFFICE365API", "OFFICE 365 API (PHPMailer)"]
["GMAILAPI", "GMAIL API SMTP-IMAP"],
["OFFICE365API", "OFFICE 365 API SMTP-IMAP"]
]
});