0
0
mirror of https://github.com/salesagility/SuiteCRM.git synced 2024-12-22 12:28:31 +00:00
salesagility_SuiteCRM/modules/Administration/SyncInboundEmailAccounts/SyncInboundEmailAccountsSubActionHandler.php
2023-07-18 15:53:09 +01:00

532 lines
17 KiB
PHP

<?php
/**
*
* SugarCRM Community Edition is a customer relationship management program developed by
* SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc.
*
* SuiteCRM is an extension to SugarCRM Community Edition developed by SalesAgility Ltd.
* Copyright (C) 2011 - 2018 SalesAgility Ltd.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by the
* Free Software Foundation with the addition of the following permission added
* to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
* IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY
* OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along with
* this program; if not, see http://www.gnu.org/licenses or write to the Free
* Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA.
*
* You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road,
* SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "Powered by
* SugarCRM" logo and "Supercharged by SuiteCRM" logo. If the display of the logos is not
* reasonably feasible for technical reasons, the Appropriate Legal Notices must
* display the words "Powered by SugarCRM" and "Supercharged by SuiteCRM".
*/
if (!defined('sugarEntry') || !sugarEntry) {
die('Not A Valid Entry Point');
}
use SuiteCRM\Utility\SuiteValidator;
/**
* Class SyncInboundEmailAccountsSubActionHandler
*
* Separated methods specially for SyncInboundEmailAccounts sub-actions handling
*
*/
#[\AllowDynamicProperties]
class SyncInboundEmailAccountsSubActionHandler
{
/**
* @const string
*/
public const PROCESS_OUTPUT_FILE = "modules/Administration/SyncInboundEmailAccounts/sync_output.html";
/**
* @var SyncInboundEmailAccountsPage
*/
protected $sync;
/**
* @var DBManager
*/
protected $db;
/**
* SyncInboundEmailAccountsSubActionHandler constructor.
*
* Handle sub-action for Sync Inbound Email Accounts
*
* @param SyncInboundEmailAccountsPage $sync
* @throws SyncInboundEmailAccountsException
* @throws SyncInboundEmailAccountsNoMethodException
*/
public function __construct(SyncInboundEmailAccountsPage $sync)
{
global $mod_strings;
$this->sync = $sync;
try {
$this->db = DBManagerFactory::getInstance();
$subAction = $this->getRequestedSubAction();
switch ($subAction) {
case 'index':
$this->action_Index();
break;
case 'sync':
$this->action_Sync();
break;
default:
throw new SyncInboundEmailAccountsNoMethodException(
"trying to call an unsupported method: " . $subAction
);
}
} catch (SyncInboundEmailAccountsException $e) {
$code = $e->getCode();
switch ($code) {
case SyncInboundEmailAccountsException::PROCESS_OUTPUT_CLEANUP_ERROR:
$this->sync->showOutput($mod_strings['LBL_PROCESS_OUTPUT_CLEANUP_ERROR']);
break;
case SyncInboundEmailAccountsException::PROCESS_OUTPUT_WRITE_ERROR:
$this->sync->showOutput($mod_strings['LBL_PROCESS_OUTPUT_WRITE_ERROR']);
break;
default:
throw new SyncInboundEmailAccountsException(
"Unknown error in sync process, see previous exception",
SyncInboundEmailAccountsException::UNKNOWN_ERROR,
$e
);
}
}
}
/**
* Return the requested sub action, use $_REQUEST['method']
* or an 'index' string by default if request was empty or incorrect
*
* @return string
* @throws SyncInboundEmailAccountsInvalidMethodTypeException
*/
protected function getRequestedSubAction()
{
$ret = "index";
// handle requested sub-action in method parameter
if (isset($_REQUEST['method']) && $_REQUEST['method']) {
$ret = $_REQUEST['method'];
// validate for correct method
if (!is_string($ret)) {
throw new SyncInboundEmailAccountsInvalidMethodTypeException(
"Method name should be a string but received type is: " . gettype($ret)
);
}
}
return $ret;
}
/**
* Default 'index' action, shows the main form
*
*/
protected function action_Index()
{
// fetch data to view
$ieList = $this->getInboundEmailRows();
// show sync-form
$this->sync->showForm($ieList);
}
/**
* Return all results for non-deleted active inbound email account
* in an inbound email account id indexed array
*
* @return array
* @throws SyncInboundEmailAccountsEmptyException
*/
protected function getInboundEmailRows()
{
$ret = $this->select("SELECT * FROM inbound_email WHERE status='Active' AND deleted = 0;");
return $ret;
}
/**
* @param string $emailId
* @return bool|SugarBean
*/
protected function getEmailBean($emailId)
{
$email = BeanFactory::getBean('Emails', $emailId);
return $email;
}
/**
* @param string $ieId
* @return bool|SugarBean
*/
protected function getInboundEmailBean($ieId)
{
$ie = BeanFactory::getBean('InboundEmail', $ieId);
return $ie;
}
/**
* @throws SyncInboundEmailAccountsException
* @throws SyncInboundEmailAccountsInvalidSubActionArgumentsException
* @throws SyncInboundEmailException
*/
protected function action_Sync()
{
global $mod_strings;
// make sure there is no time limit
// so we will having enough time to sync everything
set_time_limit(0);
$this->cleanup();
$this->output($mod_strings['LBL_SYNC_MESSAGE']);
$ieList = $this->getRequestedInboundEmailAccounts();
foreach ($ieList as $ieId) {
// TODO: scrm-539 - BeanFactory::getBean() return value is SugarBean|bool but never can be (bool)true, it may cause confusion in future
if ($ie = $this->getInboundEmailBean($ieId)) {
$this->output(sprintf($mod_strings['LBL_SYNC_PROCESSING'], $ie->name));
try {
$IMAPHeaders = $this->getEmailHeadersOfIMAPServer($ie);
$emailIds = $this->getEmailIdsOfInboundEmail($ieId);
$updated = 0;
foreach ($emailIds as $emailId => $emailData) {
$e = $this->getEmailBean($emailId);
if ($e !== false) {
$e->orphaned = $this->isOrphanedEmail($e, $IMAPHeaders);
if ($e->uid = $this->getIMAPUID($e->message_id, $IMAPHeaders)) {
if ($e->save()) {
$updated++;
}
// todo: scrm-539 handle if bean save failed
}
// todo: scrm-539 handle if there is no uid
}
}
$this->output(sprintf($mod_strings['LBL_SYNC_UPDATED'], $updated));
} catch (SyncInboundEmailAccountsIMapConnectionException $e) {
$GLOBALS['log']->warn($e->getMessage());
$this->output($mod_strings['LBL_SYNC_ERROR_CONN']);
} catch (SyncInboundEmailAccountsEmptyException $e) {
$this->output($mod_strings['LBL_SYNC_NO_EMAIL']);
}
$this->handleIMAPErrors($ie);
} else {
$this->output($mod_strings['LBL_IE_NOT_FOUND']);
$GLOBALS['log']->debug("Inbound Email Account record not found, please check the record still exists and non-deleted: " . $ieId);
}
}
$this->output($mod_strings['LBL_SYNC_DONE']);
$output = file_get_contents(self::PROCESS_OUTPUT_FILE);
$this->cleanup();
$this->sync->showOutput($output);
die();
}
/**
* @throws SyncInboundEmailAccountsException
*/
protected function handleIMAPErrors(InboundEmail $ie)
{
global $mod_strings;
$errs = $ie->getImap()->getErrors();
if ($errs) {
foreach ($errs as $err) {
$GLOBALS['log']->error("IMAP error detected: " . $err);
}
$this->output($mod_strings['LBL_SYNC_ERROR_FOUND']);
}
$warns = $ie->getImap()->getAlerts();
if ($warns) {
foreach ($warns as $warn) {
$GLOBALS['log']->warn("IMAP error detected: " . $warn);
}
$this->output($mod_strings['LBL_SYNC_ALERT_FOUND']);
}
}
/**
* @throws SyncInboundEmailAccountsException
*/
protected function cleanup()
{
if (file_exists(self::PROCESS_OUTPUT_FILE)) {
if (!unlink(self::PROCESS_OUTPUT_FILE)) {
throw new SyncInboundEmailAccountsException(
"Unable to cleanup output file. Please check permission..",
SyncInboundEmailAccountsException::PROCESS_OUTPUT_CLEANUP_ERROR
);
}
}
}
/**
* @param $msg
* @throws SyncInboundEmailAccountsException
*/
protected function output($msg)
{
$msg = "{$msg}<br>";
if (false === file_put_contents(self::PROCESS_OUTPUT_FILE, $msg, FILE_APPEND)) {
throw new SyncInboundEmailAccountsException(
"Unable to write output file. Please check permission..",
SyncInboundEmailAccountsException::PROCESS_OUTPUT_WRITE_ERROR
);
}
}
/**
* @param $emailMD5
* @param $IMAPHeaders
* @return null|int
*/
protected function getIMAPUID($emailMD5, $IMAPHeaders)
{
foreach ($IMAPHeaders as $header) {
if ($header->message_id_md5 == $emailMD5) {
return $header->imap_uid;
}
}
return null;
}
/**
* @param string $ieId
* @return array
* @throws SyncInboundEmailAccountsEmptyException
* @throws SyncInboundEmailException
*/
protected function getEmailIdsOfInboundEmail($ieId)
{
$isValidator = new SuiteValidator();
if (!$isValidator->isValidId($ieId)) {
throw new SyncInboundEmailException("Invalid Inbound Email ID");
}
$query = "SELECT id FROM emails WHERE mailbox_id = '{$ieId}' AND deleted = 0;";
$emailIds = $this->select($query);
return $emailIds;
}
/**
* @param $query
* @return array
* @throws SyncInboundEmailAccountsEmptyException
*/
protected function select($query)
{
// run sql select, grab results into an array and pass back in return
$ret = array();
$r = $this->db->query($query);
while ($e = $this->db->fetchByAssoc($r)) {
$ret[$e['id']] = $e;
}
if (empty($ret)) {
throw new SyncInboundEmailAccountsEmptyException("No imported related Email to Inbound Email Account");
}
return $ret;
}
/**
* @param InboundEmail $ie
* @param bool $test
* @param bool $force
* @param null $useSsl
* @return mixed
* @throws SyncInboundEmailAccountsIMapConnectionException
*/
protected function getEmailHeadersOfIMAPServer(InboundEmail $ie, $test = false, $force = false, $useSsl = null)
{
// ---------- CONNECT TO IMAP ------------
// override $_REQUEST['ssl'] as an argument for
// old one method InboundEmails::connectMailserver()
// to make sure the behavior is same
if (null != $useSsl) {
$_REQUEST['ssl'] = $useSsl;
}
// connect to mail server view old method
// TODO: SCRM-539 check first, may we have to restore the folder name to INBOX
$results = $ie->connectMailserver($test, $force);
// handle the error..
if ($results !== "true") {
throw new SyncInboundEmailAccountsIMapConnectionException("Connection failed to IMap ({$ie->name}): " . $results);
}
// ------------- READ IMAP EMAIL-HEADERS AND CALCULATE MD5 BASED MESSAGE_IDs ----------------
$imap_uids = $ie->getImap()->sort(SORTDATE, 0, SE_UID);
$headers = array();
foreach ($imap_uids as $imap_uid) {
$msgNo = $ie->getImap()->getMessageNo((int)$imap_uid);
$headers[$imap_uid] = $ie->getImap()->getHeaderInfo($msgNo);
$headers[$imap_uid]->imap_uid = $imap_uid;
$headers[$imap_uid]->imap_msgid_int = (int)$msgNo;
}
foreach ($headers as &$header) {
$header->message_id_md5 = $this->getCompoundMessageIdMD5($ie, $header->imap_uid);
}
// ------------ IMAP CLOSE -------------
$ie->getImap()->close();
return $headers;
}
/**
* @param $header
* @param InboundEmail $ie
* @param $uid
* @param null $msgNo
* @return string
*/
protected function getCompoundMessageIdMD5(InboundEmail $ie, $uid, $msgNo = null)
{
if (empty($msgNo) && !empty($uid)) {
$msgNo = $ie->getImap()->getMessageNo((int)$uid);
}
$header = $ie->getImap()->getHeaderInfo($msgNo);
$fullHeader = $ie->getImap()->fetchHeader($msgNo);
$message_id = $header->message_id;
$deliveredTo = $ie->id;
$matches = array();
preg_match('/(delivered-to:|x-real-to:){1}\s*(\S+)\s*\n{1}/im', (string) $fullHeader, $matches);
if (count($matches)) {
$deliveredTo = $matches[2];
}
if (empty($message_id) || !isset($message_id)) {
$GLOBALS['log']->debug('*********** NO MESSAGE_ID.');
$message_id = $ie->getMessageId($header);
}
// generate compound messageId
$compoundMessageId = trim($message_id) . trim($deliveredTo);
// if the length > 255 then md5 it so that the data will be of smaller length
//if (strlen($compoundMessageId) > 255) {
$compoundMessageId = md5($compoundMessageId);
//} // if
if (empty($compoundMessageId)) {
return null; //throw new Exception('????');
} // if
//$counter++;
$potentials = clean_xss($compoundMessageId, false);
if (is_array($potentials) && !empty($potentials)) {
foreach ($potentials as $bad) {
$compoundMessageId = str_replace($bad, "", $compoundMessageId);
}
}
return $compoundMessageId;
}
/**
* @param Email $e
* @param $IMAPheaders
* @return bool
*/
protected function isOrphanedEmail(Email $e, $IMAPheaders)
{
foreach ($IMAPheaders as $header) {
if ($header->message_id_md5 == $e->message_id) {
return false;
}
}
return true;
}
/**
* This function only for main form handling,
* calling by sync action to get selected inbound email accounts
*
* @return mixed
* @throws SyncInboundEmailAccountsInvalidSubActionArgumentsException
*/
protected function getRequestedInboundEmailAccounts()
{
// validate for selected inbound email(s)
if (!isset($_REQUEST['ie-sel'])) {
// it's should be in the request
throw new SyncInboundEmailAccountsInvalidSubActionArgumentsException("Invalid action parameter");
}
$ieSel = $_REQUEST['ie-sel'];
if (!$ieSel) {
// if there is not any selected, just fill out with all inbound email
$ieSel = array_keys($this->getInboundEmailRows());
}
return $ieSel;
}
}