Merged in feature/PMCORE-3929 (pull request #8525)
PMCORE-3929 MS Modern Authentication / GMAIL API IMAP for ABE
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2030
composer.lock
generated
2030
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user