Commit fffed6f1 authored by Jean-Laurent DUZANT's avatar Jean-Laurent DUZANT
Browse files

Merge branch 'feat/22643/develop' into 'develop'

[22643] Develop : EWSMailCapture - implémentation du protocole OAUTH 2.0

See merge request maarch/MaarchCapture!53
parents ffb5562b 287ff9ee
......@@ -59,6 +59,26 @@
</step> -->
</workflow>
</batch>
<!-- EWSMAIL TO MAARCHCOURRIER SAMPLE -->
<batch name="MAARCH_EWSMAIL_TO_MC" directory="/opt/maarch/MaarchCapture/files/" id="{batchname}-{timestamp}-{rand}" lock="0">
<workflow name="MAARCH_EWSMAIL_TO_MC" debug="true" logMode="Maarch" maarchLogParam="/var/www/html/MaarchCapture/apps/maarch_entreprise/xml/log4php.xml" maarchLoggerName="loggerTechnique">
<step function="CaptureMails" module="EWSMailCapture" name="CAPTURE_MAIL_1">
<input name="account">account_1</input>
<input name="action">move</input> <!-- move, delete ou none-->
<input name="configFile">EWSMailCapture_standard_sample.xml</input>
<input name="folder">maarch/purge</input> <!-- utilisé si action move -->
<input name="addHeaderInMailContent">true</input>
</step>
<step name="SendToMaarch" module="MaarchWSClient" function="processBatch">
<input name="WS">MaarchRestWS</input>
<input name="Process">MaarchRestWSProcessFromMail</input>
<input name="CatchError">true</input>
<input name="configFile">MaarchWSClient_standard_sample.xml</input>
</step>
</workflow>
</batch>
<!-- RECONCILE WITH QRCODE-->
<batch name="RECONCILE_QR" directory="/opt/maarch/MaarchCapture/files/" id="{batchname}-{timestamp}-{rand}" lock="0">
<workflow name="RECONCILE_QR" debug="true" logMode="Maarch" maarchLogParam="/opt/maarch/MaarchCapture/config/log4php/log4php.xml.default" maarchLoggerName="loggerTechnique">
......
......@@ -26,6 +26,26 @@
</step>
</workflow>
</batch>
<!-- EWSMAIL TO MAARCHCOURRIER SAMPLE -->
<batch name="MAARCH_EWSMAIL_TO_MC" directory="F:\maarch\MaarchCapture\files\" id="{batchname}-{timestamp}-{rand}" lock="0">
<workflow name="MAARCH_EWSMAIL_TO_MC" debug="true" logMode="Maarch">
<step function="CaptureMails" module="EWSMailCapture" name="CAPTURE_MAIL_1">
<input name="account">account_1</input>
<input name="action">move</input> <!-- move, delete ou none-->
<input name="configFile">EWSMailCapture_standard_sample.xml</input>
<input name="folder">maarch/purge</input> <!-- utilisé si action move -->
<input name="addHeaderInMailContent">true</input>
</step>
<step name="SendToMaarch" module="MaarchWSClient" function="processBatch">
<input name="WS">MaarchRestWS</input>
<input name="Process">MaarchRestWSProcessFromMail</input>
<input name="CatchError">true</input>
<input name="configFile">MaarchWSClient_standard_sample.xml</input>
</step>
</workflow>
</batch>
<!-- MAIL TO MAARCHCOURRIER SAMPLE -->
<batch name="MAARCH_MAIL_TO_MC" directory="F:\maarch\MaarchCapture\files\" id="{batchname}-{timestamp}-{rand}" lock="0">
<workflow name="MAARCH_MAIL_TO_MC" debug="false" logMode="default">
......
......@@ -24,7 +24,8 @@ class ExchangeItem {
private $toAddress;
private $ccAddress;
public function __construct($message, $client) {
public function __construct($message, $client)
{
$this->itemId = $message->ItemId->Id;
$this->senderName = $message->From->Mailbox->Name;
$this->senderEmailAddress = $message->From->Mailbox->EmailAddress;
......@@ -56,7 +57,8 @@ class ExchangeItem {
}
}
public function get($key) {
public function get($key)
{
switch ($key) {
case 'date':
return $this->getISODate();
......@@ -88,69 +90,84 @@ class ExchangeItem {
}
}
public function getItemId() {
public function getItemId()
{
return $this->itemId;
}
public function setItemId($itemId) {
public function setItemId($itemId)
{
$this->itemId = $itemId;
}
public function getSenderName() {
public function getSenderName()
{
return $this->senderName;
}
public function getSenderEmailAddress() {
public function getSenderEmailAddress()
{
return $this->senderEmailAddress;
}
public function getToAddress() {
public function getToAddress()
{
return $this->toAddress;
}
public function getCcAddress() {
public function getCcAddress()
{
return $this->ccAddress;
}
public function getSubject($attI = null) {
public function getSubject($attI = null)
{
if (is_int($attI) && $attI >= 0 && $attI < count($this->attachments)) {
return $this->subject . ' (' . ($attI + 1) . '/' . count($this->attachments) . ') : ' . $this->attachments[$attI]['name'];
}
return $this->subject;
}
public function getISODate() {
public function getISODate()
{
return $this->isoDate;
}
public function getBody() {
public function getBody()
{
return $this->body;
}
public function getImportance() {
public function getImportance()
{
return $this->importance;
}
public function isUrgent() {
public function isUrgent()
{
return $this->urgent;
}
public function getAttachments() {
public function getAttachments()
{
return $this->attachments;
}
public function getAttachmentsCount() {
public function getAttachmentsCount()
{
return count($this->attachments);
}
public function getInlineAttachmentsCount() {
public function getInlineAttachmentsCount()
{
return $this->inlineAttachmentsCount;
}
/**
* this function fetches attachments ids, names and contents
*/
private function populateAttachments($rawAttachments, $client) {
private function populateAttachments($rawAttachments, $client)
{
$this->attachments = [];
if (empty($rawAttachments)) {
return;
......
......@@ -20,6 +20,8 @@ use jamesiarmes\PhpEws\Type\FolderResponseShapeType;
use jamesiarmes\PhpEws\Type\ItemResponseShapeType;
use jamesiarmes\PhpEws\Enumeration\DefaultShapeNamesType;
use jamesiarmes\PhpEws\Enumeration\DisposalType;
use jamesiarmes\PhpEws\Type\ConnectingSIDType;
use jamesiarmes\PhpEws\Type\ExchangeImpersonationType;
/**
* ExchangeMailbox class: an Exchage mailbox wrapper
......@@ -29,14 +31,105 @@ use jamesiarmes\PhpEws\Enumeration\DisposalType;
class ExchangeMailbox {
private $client;
private $folders;
private $logFile;
public function __construct($host, $address, $password, $version) {
public const BASIC_AUTH = "Basic";
public const O_AUTH_2 = "OAuth2";
private const BASE_TOKEN_URL = 'https://login.microsoftonline.com/';
public function __construct(array $args)
{
$batch = $_SESSION['capture']->Batch;
$this->logFile = $batch->directory . DIRECTORY_SEPARATOR . 'EWSMailCapture.log';
switch ($args['authMethod']) {
case ExchangeMailbox::BASIC_AUTH:
$this->writeLog(sprintf("Authenticating using '%s' method ", ExchangeMailbox::BASIC_AUTH));
$this->initBasicAuth($args['mailbox'], $args['username'], $args['password'], $args['exchangeversion']);
try {
$this->discoverFolders();
} catch (\Exception $e) {
$log = "Exception occurred while trying Basic auth, you may want to setup OAuth2:\n\n" . (string)$e;
$this->writeLog($log);
$_SESSION['capture']->sendError($log);
}
break;
case ExchangeMailbox::O_AUTH_2:
$this->writeLog(sprintf("Authenticating using '%s' method ", ExchangeMailbox::O_AUTH_2));
$this->initOauth2($args['mailbox'], $args['username'], $args['exchangeversion'], $args['tenantID'], $args['clientID'], $args['clientSecret']);
try {
$this->discoverFolders();
} catch (\Exception $e) {
$log = "Exception occurred while trying OAuth2:\n\n" . (string)$e;
$this->writeLog($log);
$_SESSION['capture']->sendError($log);
}
break;
default:
$log = sprintf("\n\nUnknown auth method '%s'.\nAvailable auth methods are %s or %s\n\n", $args['authMethod'], ExchangeMailbox::BASIC_AUTH, ExchangeMailbox::O_AUTH_2);
$this->writeLog($log);
$_SESSION['capture']->sendError($log);
}
}
private function initBasicAuth($host, $address, $password, $version)
{
$this->client = new Client($host, $address, $password, $version);
$this->client->setCurlOptions([CURLOPT_SSL_VERIFYPEER => false]);
$this->discoverFolders();
}
private function discoverFolders() {
private function initOauth2($host, $address, $version, $tenantID, $clientID, $clientSecret)
{
$curl = curl_init(ExchangeMailbox::BASE_TOKEN_URL . $tenantID . '/oauth2/v2.0/token');
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true,
CURLOPT_POSTFIELDS => [
'grant_type' => 'client_credentials',
'client_id' => $clientID,
'client_secret' => $clientSecret,
'scope' => 'https://' . $host . '/.default'
]
]);
$response = curl_exec($curl);
curl_close($curl);
$response = explode("\r\n\r\n", $response);
$responseHeaders = $response[0] ?? '';
$responseBody = $response[1] ?? '';
$responseBody = json_decode($responseBody, true);
if (empty($responseBody['access_token'])) {
$this->writeLog("Error while fetching access token, return transfer written to " . $this->logFile);
$error = "\n\nHeaders: " . $responseHeaders;
if (!empty($responseBody['error'])) {
$error .= "\n\nError: " . $responseBody['error'] . "\n\nError description: " . ($responseBody['error_description'] ?? '');
}
$this->writeLog($error);
$_SESSION['capture']->sendError("Error while fetching access token, return transfer written to " . $this->logFile . "\n");
}
$this->client = new Client($host, $address, '-', $version);
$this->client->setCurlOptions([
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $responseBody['access_token']]
]);
$exim = new ExchangeImpersonationType();
$csid = new ConnectingSIDType();
$csid->PrimarySmtpAddress = $address;
$exim->ConnectingSID = $csid;
$this->client->setImpersonation($exim);
}
private function writeLog($str)
{
$str = '[' . date('c') . '] ExchangeMailbox: ' . $str . PHP_EOL;
echo $str;
file_put_contents($this->logFile, $str, FILE_APPEND);
}
private function discoverFolders()
{
$request = new FindFolderType();
$request->Traversal = FolderQueryTraversalType::DEEP;
$request->ParentFolderIds = new NonEmptyArrayOfBaseFolderIdsType();
......@@ -87,19 +180,21 @@ class ExchangeMailbox {
}
}
private function getFolderIdByName($folderName) {
private function getFolderIdByName($folderName)
{
$folderName = trim($folderName, '/');
return array_key_exists($folderName, $this->folders) ? $this->folders[$folderName] : null;
}
public function getItemsByFolderName($folderName) {
public function getItemsByFolderName($folderName)
{
$folderId = $this->getFolderIdByName($folderName);
$useInbox = false;
if (mb_strtolower($folderName) === 'inbox') {
$useInbox = true;
}
if (!$useInbox && empty($folderId)) {
return [];
return false;
}
$request = new FindItemType();
$request->ParentFolderIds = new NonEmptyArrayOfBaseFolderIdsType();
......@@ -137,14 +232,15 @@ class ExchangeMailbox {
return $items;
}
public function moveItemToNamedFolder(&$item, $folderName) {
public function moveItemToNamedFolder(&$item, $folderName)
{
$folderId = $this->getFolderIdByName($folderName);
$useInbox = false;
if (mb_strtolower($folderName) === 'inbox') {
$useInbox = true;
}
if (!$useInbox && empty($folderId)) {
return null;
return false;
}
$request = new MoveItemType();
$request->ToFolderId = new TargetFolderIdType();
......@@ -168,7 +264,8 @@ class ExchangeMailbox {
return true;
}
public function deleteItem($item) {
public function deleteItem($item)
{
$request = new DeleteItemType();
$request->DeleteType = DisposalType::MOVE_TO_DELETED_ITEMS;
$request->ItemIds = new NonEmptyArrayOfBaseItemIdsType();
......
......@@ -28,9 +28,25 @@ class EWSMailCapture
$captureFolder = preg_replace('/\{.+\}(.+)/', '\1', $mailbox);
$mailbox = preg_replace('/\{(.+)\}.+/', '\1', $mailbox);
$username = (string) ($accountConfig->xpath('username')[0] ?? '');
$password = (string) ($accountConfig->xpath('password')[0] ?? '');
$exchangeVersion = (string) ($accountConfig->xpath('exchangeversion')[0] ?? '');
$exchangeMailboxArgs = [];
$exchangeMailboxArgs['mailbox'] = $mailbox;
$exchangeMailboxArgs['username'] = (string) ($accountConfig->xpath('username')[0] ?? '');
$exchangeMailboxArgs['password'] = (string) ($accountConfig->xpath('password')[0] ?? '');
$exchangeMailboxArgs['tenantID'] = (string) ($accountConfig->xpath('tenantID')[0] ?? '');
$exchangeMailboxArgs['clientID'] = (string) ($accountConfig->xpath('clientID')[0] ?? '');
$exchangeMailboxArgs['clientSecret'] = (string) ($accountConfig->xpath('clientSecret')[0] ?? '');
if (!empty($exchangeMailboxArgs['password'])) {
$exchangeMailboxArgs['authMethod'] = ExchangeMailbox::BASIC_AUTH;
} elseif (!empty($exchangeMailboxArgs['tenantID']) && !empty($exchangeMailboxArgs['clientID']) && !empty($exchangeMailboxArgs['clientSecret'])) {
$exchangeMailboxArgs['authMethod'] = ExchangeMailbox::O_AUTH_2;
} else {
$log = sprintf("\n\nUnable to set auth method\nCheck the account '%s' configuration in %s\n\n", $account, __DIR__ . DIRECTORY_SEPARATOR . $configFile);
$this->writeLog($log);
$_SESSION['capture']->sendError($log);
}
$exchangeMailboxArgs['exchangeversion'] = (string) ($accountConfig->xpath('exchangeversion')[0] ?? '');
$messageRules = $xmlConfig->xpath('/EWSMailCapture/messagerules/messagerule') ?: [];
$attachmentRules = $xmlConfig->xpath('/EWSMailCapture/attachmentrules/attachmentrule') ?: [];
......@@ -75,15 +91,35 @@ class EWSMailCapture
}
}
if (empty($mailbox) || empty($captureFolder) || empty($exchangeVersion) || empty($username) || empty($password)) {
$_SESSION['capture']->sendError('MS Exchange mailbox configuration is invalid!');
if ($exchangeMailboxArgs['authMethod'] == ExchangeMailbox::BASIC_AUTH && (
empty($exchangeMailboxArgs['mailbox'])
|| empty($exchangeMailboxArgs['exchangeversion'])
|| empty($exchangeMailboxArgs['username'])
|| empty($exchangeMailboxArgs['password'])
|| empty($captureFolder)
)) {
$_SESSION['capture']->sendError(sprintf("MS Exchange mailbox configuration for %s is invalid!\n%s", ExchangeMailbox::BASIC_AUTH, json_encode($exchangeMailboxArgs)));
} elseif ($exchangeMailboxArgs['authMethod'] == ExchangeMailbox::O_AUTH_2 && (
empty($exchangeMailboxArgs['mailbox'])
|| empty($exchangeMailboxArgs['exchangeversion'])
|| empty($exchangeMailboxArgs['username'])
|| empty($exchangeMailboxArgs['tenantID'])
|| empty($exchangeMailboxArgs['clientID'])
|| empty($exchangeMailboxArgs['clientSecret'])
|| empty($captureFolder)
)) {
$_SESSION['capture']->sendError(sprintf("\n\nMS Exchange mailbox configuration for %s is invalid!\n%s\n\n", ExchangeMailbox::O_AUTH_2, json_encode($exchangeMailboxArgs)));
}
$ewsMailbox = new ExchangeMailbox($mailbox, $username, $password, $exchangeVersion);
$ewsMailbox = new ExchangeMailbox($exchangeMailboxArgs);
$ewsItems = $ewsMailbox->getItemsByFolderName($captureFolder);
if ($ewsItems === false) {
$this->writeLog('ERROR: could not get items: mailbox folder \'' . $captureFolder . '\' does not exist.');
$_SESSION['capture']->sendError('could not get items: mailbox folder \'' . $captureFolder . '\' does not exist.');
}
$itemCount = count($ewsItems);
$this->writeLog($itemCount . ' messages in mailbox');
$this->writeLog($itemCount . ' messages in mailbox folder \'' . $captureFolder . '\'');
foreach ($ewsItems as $ewsItemI => $ewsItem) {
$this->writeLog('processing email ' . ($ewsItemI + 1) . '/' . $itemCount . ': ' . $ewsItem->getSubject());
......@@ -161,7 +197,11 @@ class EWSMailCapture
if ($action === 'move') {
$this->writeLog('moving email to purge folder: ' . $folder);
$ewsMailbox->moveItemToNamedFolder($ewsItem, $folder);
$moved = $ewsMailbox->moveItemToNamedFolder($ewsItem, $folder);
if ($moved === false) {
$this->writeLog('ERROR: could not move item: mailbox folder \'' . $folder . '\' does not exist.');
$_SESSION['capture']->sendError('could not move item: mailbox folder \'' . $folder . '\' does not exist.');
}
} elseif ($action === 'delete') {
$this->writeLog('moving email to trash');
$ewsMailbox->deleteItem($ewsItem);
......@@ -253,7 +293,11 @@ class EWSMailCapture
$folder = (string) ($messageRule->attributes()['folder'] ?? '');
if (!empty($folder)) {
$this->writeLog('moving mail to ' . $folder);
$ewsMailbox->moveItemToNamedFolder($ewsItem, $folder);
$moved = $ewsMailbox->moveItemToNamedFolder($ewsItem, $folder);
if ($moved === false) {
$this->writeLog('ERROR: could not move item: mailbox folder \'' . $folder . '\' does not exist.');
$_SESSION['capture']->sendError('could not move item: mailbox folder \'' . $folder . '\' does not exist.');
}
} else {
$this->writeLog('WARNING: move action with no specified folder!');
}
......@@ -292,4 +336,4 @@ class EWSMailCapture
echo $str;
file_put_contents($this->logFile, $str, FILE_APPEND);
}
}
\ No newline at end of file
}
......@@ -3,8 +3,7 @@
<accounts>
<account name="account_1" >
<mailbox>{mail.name.com}INBOX</mailbox>
<username>your-email@name.com</username>
<password>******</password>
<exchangeversion>Exchange2016</exchangeversion>
<!--
exchangeversion must be one of:
Exchange2007
......@@ -16,12 +15,23 @@
Exchange2013
Exchange2013_SP1
Exchange2016
-->
<exchangeversion>Exchange2016</exchangeversion>
-->
<!-- authentification : renseigner uniquement les balises du mode utilisé, supprimer ou commenter les autres -->
<!-- pour l’authentification Basic -->
<username>your-email@name.com</username>
<password>******</password>
<!-- pour l’authentification OAuth2 -->
<username>your-email@name.com</username>
<tenantID>******</tenantID>
<clientID>******</clientID>
<clientSecret>******</clientSecret>
</account>
</accounts>
<messagerules>
<messagerule name="NO_CC_IN_SUBJECT" info="subject" action="none" folder="tmp" op="contains">Cc</messagerule>
<!-- <messagerule name="NO_CC_IN_SUBJECT" info="subject" action="none" folder="tmp" op="contains">Cc</messagerule> -->
</messagerules>
<messageoutputs>
<messageoutput name="doc_date" info="date"/>
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment