PageRenderTime 50ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 1ms

/modules/InboundEmail/InboundEmail.php

https://bitbucket.org/cviolette/sugarcrm
PHP | 6480 lines | 4383 code | 783 blank | 1314 comment | 812 complexity | 4ed6b6cb6b0602abf3a6f2907774d78b MD5 | raw file
Possible License(s): LGPL-2.1, MPL-2.0-no-copyleft-exception, BSD-3-Clause
  1. <?php
  2. if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
  3. /*********************************************************************************
  4. * SugarCRM Community Edition is a customer relationship management program developed by
  5. * SugarCRM, Inc. Copyright (C) 2004-2012 SugarCRM Inc.
  6. *
  7. * This program is free software; you can redistribute it and/or modify it under
  8. * the terms of the GNU Affero General Public License version 3 as published by the
  9. * Free Software Foundation with the addition of the following permission added
  10. * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
  11. * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY
  12. * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
  13. *
  14. * This program is distributed in the hope that it will be useful, but WITHOUT
  15. * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  16. * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
  17. * details.
  18. *
  19. * You should have received a copy of the GNU Affero General Public License along with
  20. * this program; if not, see http://www.gnu.org/licenses or write to the Free
  21. * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  22. * 02110-1301 USA.
  23. *
  24. * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road,
  25. * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com.
  26. *
  27. * The interactive user interfaces in modified source and object code versions
  28. * of this program must display Appropriate Legal Notices, as required under
  29. * Section 5 of the GNU Affero General Public License version 3.
  30. *
  31. * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
  32. * these Appropriate Legal Notices must retain the display of the "Powered by
  33. * SugarCRM" logo. If the display of the logo is not reasonably feasible for
  34. * technical reasons, the Appropriate Legal Notices must display the words
  35. * "Powered by SugarCRM".
  36. ********************************************************************************/
  37. require_once('include/OutboundEmail/OutboundEmail.php');
  38. function this_callback($str) {
  39. foreach($str as $match) {
  40. $ret .= chr(hexdec(str_replace("%","",$match)));
  41. }
  42. return $ret;
  43. }
  44. /**
  45. * Stub for certain interactions;
  46. */
  47. class temp {
  48. var $name;
  49. }
  50. class InboundEmail extends SugarBean {
  51. // module specific
  52. var $conn;
  53. var $purifier; // HTMLPurifier object placeholder
  54. var $email;
  55. // fields
  56. var $id;
  57. var $deleted;
  58. var $date_entered;
  59. var $date_modified;
  60. var $modified_user_id;
  61. var $created_by;
  62. var $created_by_name;
  63. var $modified_by_name;
  64. var $name;
  65. var $status;
  66. var $server_url;
  67. var $email_user;
  68. var $email_password;
  69. var $port;
  70. var $service;
  71. var $mailbox;
  72. var $mailboxarray;
  73. var $delete_seen;
  74. var $mailbox_type;
  75. var $template_id;
  76. var $stored_options;
  77. var $group_id;
  78. var $is_personal;
  79. var $groupfolder_id;
  80. // email 2.0
  81. var $pop3socket;
  82. var $outboundInstance; // id to outbound_email instance
  83. var $autoImport;
  84. var $iconFlagged = "F";
  85. var $iconDraft = "D";
  86. var $iconAnswered = "A";
  87. var $iconDeleted = "del";
  88. var $isAutoImport = false;
  89. var $smarty;
  90. var $attachmentCount = 0;
  91. var $tempAttachment = array();
  92. var $unsafeChars = array("&", "!", "'", '"', '\\', '/', '<', '>', '|', '$',);
  93. var $currentCache;
  94. var $defaultSort = 'date';
  95. var $defaultDirection = "DESC";
  96. var $hrSort = array(
  97. 0 => 'flagged',
  98. 1 => 'status',
  99. 2 => 'from',
  100. 3 => 'subj',
  101. 4 => 'date',
  102. );
  103. var $hrSortLocal = array(
  104. 'flagged' => 'flagged',
  105. 'status' => 'answered',
  106. 'from' => 'fromaddr',
  107. 'subject' => 'subject',
  108. 'date' => 'senddate',
  109. );
  110. // default attributes
  111. var $transferEncoding = array(0 => '7BIT',
  112. 1 => '8BIT',
  113. 2 => 'BINARY',
  114. 3 => 'BASE64',
  115. 4 => 'QUOTED-PRINTABLE',
  116. 5 => 'OTHER'
  117. );
  118. // object attributes
  119. var $compoundMessageId; // concatenation of messageID and deliveredToEmail
  120. var $serverConnectString;
  121. var $disable_row_level_security = true;
  122. var $InboundEmailCachePath;
  123. var $InboundEmailCacheFile = 'InboundEmail.cache.php';
  124. var $object_name = 'InboundEmail';
  125. var $module_dir = 'InboundEmail';
  126. var $table_name = 'inbound_email';
  127. var $new_schema = true;
  128. var $process_save_dates = true;
  129. var $order_by;
  130. var $db;
  131. var $dbManager;
  132. var $field_defs;
  133. var $column_fields;
  134. var $required_fields = array('name' => 'name',
  135. 'server_url' => 'server_url',
  136. 'mailbox' => 'mailbox',
  137. 'user' => 'user',
  138. 'port' => 'port',
  139. );
  140. var $imageTypes = array("JPG", "JPEG", "GIF", "PNG");
  141. var $inlineImages = array(); // temporary space to store ID of inlined images
  142. var $defaultEmailNumAutoreplies24Hours = 10;
  143. var $maxEmailNumAutoreplies24Hours = 10;
  144. // custom ListView attributes
  145. var $mailbox_type_name;
  146. var $global_personal_string;
  147. // service attributes
  148. var $tls;
  149. var $ca;
  150. var $ssl;
  151. var $protocol;
  152. var $keyForUsersDefaultIEAccount = 'defaultIEAccount';
  153. // prefix to use when importing inlinge images in emails
  154. public $imagePrefix;
  155. /**
  156. * Sole constructor
  157. */
  158. function InboundEmail() {
  159. $this->InboundEmailCachePath = sugar_cached('modules/InboundEmail');
  160. $this->EmailCachePath = sugar_cached('modules/Emails');
  161. parent::SugarBean();
  162. if(function_exists("imap_timeout")) {
  163. /*
  164. * 1: Open
  165. * 2: Read
  166. * 3: Write
  167. * 4: Close
  168. */
  169. imap_timeout(1, 60);
  170. imap_timeout(2, 60);
  171. imap_timeout(3, 60);
  172. }
  173. $this->smarty = new Sugar_Smarty();
  174. $this->overview = new Overview();
  175. $this->imagePrefix = "{$GLOBALS['sugar_config']['site_url']}/cache/images/";
  176. }
  177. /**
  178. * retrieves I-E bean
  179. * @param string id
  180. * @return object Bean
  181. */
  182. function retrieve($id, $encode=true, $deleted=true) {
  183. $ret = parent::retrieve($id,$encode,$deleted);
  184. // if I-E bean exist
  185. if (!is_null($ret)) {
  186. $this->email_password = blowfishDecode(blowfishGetKey('InboundEmail'), $this->email_password);
  187. $this->retrieveMailBoxFolders();
  188. }
  189. return $ret;
  190. }
  191. /**
  192. * wraps SugarBean->save()
  193. * @param string ID of saved bean
  194. */
  195. function save($check_notify=false) {
  196. // generate cache table for email 2.0
  197. $multiDImArray = $this->generateMultiDimArrayFromFlatArray(explode(",", $this->mailbox), $this->retrieveDelimiter());
  198. $raw = $this->generateFlatArrayFromMultiDimArray($multiDImArray, $this->retrieveDelimiter());
  199. sort($raw);
  200. //_pp(explode(",", $this->mailbox));
  201. //_ppd($raw);
  202. $raw = $this->filterMailBoxFromRaw(explode(",", $this->mailbox), $raw);
  203. $this->mailbox = implode(",", $raw);
  204. if(!empty($this->email_password)) {
  205. $this->email_password = blowfishEncode(blowfishGetKey('InboundEmail'), $this->email_password);
  206. }
  207. $ret = parent::save($check_notify);
  208. return $ret;
  209. }
  210. function filterMailBoxFromRaw($mailboxArray, $rawArray) {
  211. $newArray = array_intersect($mailboxArray, $rawArray);
  212. sort($newArray);
  213. return $newArray;
  214. } // fn
  215. /**
  216. * Overrides SugarBean's mark_deleted() to drop the related cache table
  217. * @param string $id GUID of I-E instance
  218. */
  219. function mark_deleted($id) {
  220. parent::mark_deleted($id);
  221. $q = "update inbound_email set groupfolder_id = null WHERE id = '{$id}'";
  222. $r = $this->db->query($q);
  223. $this->deleteCache();
  224. }
  225. /**
  226. * Mark cached email answered (replied)
  227. * @param string $mailid (uid for imap, message_id for pop3)
  228. */
  229. function mark_answered($mailid, $type = 'smtp') {
  230. switch ($type) {
  231. case 'smtp' :
  232. $q = "update email_cache set answered = 1 WHERE imap_uid = $mailid and ie_id = '{$this->id}'";
  233. $this->db->query($q);
  234. break;
  235. case 'pop3' :
  236. $q = "update email_cache set answered = 1 WHERE message_id = '$mailid' and ie_id = '{$this->id}'";
  237. $this->db->query($q);
  238. break;
  239. }
  240. }
  241. /**
  242. * Renames an IMAP mailbox
  243. * @param string $newName
  244. */
  245. function renameFolder($oldName, $newName) {
  246. //$this->mailbox = "INBOX"
  247. $this->connectMailserver();
  248. $oldConnect = $this->getConnectString('', $oldName);
  249. $newConnect = $this->getConnectString('', $newName);
  250. if(!imap_renamemailbox($this->conn, $oldConnect , $newConnect)) {
  251. $GLOBALS['log']->debug("***INBOUNDEMAIL: failed to rename mailbox [ {$oldConnect} ] to [ {$newConnect} ]");
  252. } else {
  253. $this->mailbox = str_replace($oldName, $newName, $this->mailbox);
  254. $this->save();
  255. $sessionFoldersString = $this->getSessionInboundFoldersString($this->server_url, $this->email_user, $this->port, $this->protocol);
  256. $sessionFoldersString = str_replace($oldName, $newName, $sessionFoldersString);
  257. $this->setSessionInboundFoldersString($this->server_url, $this->email_user, $this->port, $this->protocol, $sessionFoldersString);
  258. }
  259. }
  260. ///////////////////////////////////////////////////////////////////////////
  261. //// CUSTOM LOGIC HOOKS
  262. /**
  263. * Called from $this->getMessageText()
  264. * Allows upgrade-safe custom processing of message text.
  265. *
  266. * To use:
  267. * 1. Create a directory path: ./custom/modules/InboundEmail if it does not exist
  268. * 2. Create a file in the ./custom/InboundEmail/ folder called "getMessageText.php"
  269. * 3. Define a function named "custom_getMessageText()" that takes a string as an argument and returns a string
  270. *
  271. * @param string $msgPart
  272. * @return string
  273. */
  274. function customGetMessageText($msgPart) {
  275. $custom = "custom/modules/InboundEmail/getMessageText.php";
  276. if(file_exists($custom)) {
  277. include_once($custom);
  278. if(function_exists("custom_getMessageText")) {
  279. $GLOBALS['log']->debug("*** INBOUND EMAIL-CUSTOM_LOGIC: calling custom_getMessageText()");
  280. $msgPart = custom_getMessageText($msgPart);
  281. }
  282. }
  283. return $msgPart;
  284. }
  285. //// END CUSTOM LOGIC HOOKS
  286. ///////////////////////////////////////////////////////////////////////////
  287. ///////////////////////////////////////////////////////////////////////////
  288. //// EMAIL 2.0 SPECIFIC
  289. /**
  290. * constructs a nicely formatted version of raw source
  291. * @param int $uid UID of email
  292. * @return string
  293. */
  294. function getFormattedRawSource($uid) {
  295. global $app_strings;
  296. //if($this->protocol == 'pop3') {
  297. //$raw = $app_strings['LBL_EMAIL_VIEW_UNSUPPORTED'];
  298. //} else {
  299. if (empty($this->id)) {
  300. $q = "SELECT raw_source FROM emails_text WHERE email_id = '{$uid}'";
  301. $r = $this->db->query($q);
  302. $a = $this->db->fetchByAssoc($r);
  303. $ret = array();
  304. $raw = $this->convertToUtf8($a['raw_source']);
  305. if (empty($raw)) {
  306. $raw = $app_strings['LBL_EMAIL_ERROR_VIEW_RAW_SOURCE'];
  307. }
  308. } else {
  309. if ($this->isPop3Protocol()) {
  310. $uid = $this->getCorrectMessageNoForPop3($uid);
  311. }
  312. $raw = imap_fetchheader($this->conn, $uid, FT_UID+FT_PREFETCHTEXT);
  313. $raw .= $this->convertToUtf8(imap_body($this->conn, $uid, FT_UID));
  314. } // else
  315. $raw = to_html($raw);
  316. $raw = nl2br($raw);
  317. //}
  318. return $raw;
  319. }
  320. /**
  321. * helper method to convert text to utf-8 if necessary
  322. *
  323. * @param string $input text
  324. * @return string output text
  325. */
  326. function convertToUtf8($input)
  327. {
  328. $charset = $GLOBALS['locale']->detectCharset($input, true);
  329. // we haven't a clue due to missing package?, just return as itself
  330. if ($charset === FALSE) {
  331. return $input;
  332. }
  333. // convert if we can or must
  334. return $this->handleCharsetTranslation($input, $charset);
  335. }
  336. /**
  337. * constructs a nicely formatted version of email headers.
  338. * @param int $uid
  339. * @return string
  340. */
  341. function getFormattedHeaders($uid) {
  342. global $app_strings;
  343. //if($this->protocol == 'pop3') {
  344. // $header = $app_strings['LBL_EMAIL_VIEW_UNSUPPORTED'];
  345. //} else {
  346. if ($this->isPop3Protocol()) {
  347. $uid = $this->getCorrectMessageNoForPop3($uid);
  348. }
  349. $headers = imap_fetchheader($this->conn, $uid, FT_UID);
  350. $lines = explode("\n", $headers);
  351. $header = "<table cellspacing='0' cellpadding='2' border='0' width='100%'>";
  352. foreach($lines as $line) {
  353. $line = trim($line);
  354. if(!empty($line)) {
  355. $key = trim(substr($line, 0, strpos($line, ":")));
  356. $key = strip_tags($key);
  357. $value = trim(substr($line, strpos($line, ":") + 1));
  358. $value = to_html($value);
  359. $header .= "<tr>";
  360. $header .= "<td class='displayEmailLabel' NOWRAP><b>{$key}</b>&nbsp;</td>";
  361. $header .= "<td class='displayEmailValueWhite'>{$value}&nbsp;</td>";
  362. $header .= "</tr>";
  363. }
  364. }
  365. $header .= "</table>";
  366. //}
  367. return $header;
  368. }
  369. /**
  370. * Empties Trash folders
  371. */
  372. function emptyTrash() {
  373. global $sugar_config;
  374. $this->mailbox = $this->get_stored_options("trashFolder");
  375. if (empty($this->mailbox)) {
  376. $this->mailbox = 'INBOX.Trash';
  377. }
  378. $this->connectMailserver();
  379. $uids = imap_search($this->conn, "ALL", SE_UID);
  380. foreach($uids as $uid) {
  381. if(!imap_delete($this->conn, $uid, FT_UID)) {
  382. $lastError = imap_last_error();
  383. $GLOBALS['log']->warn("INBOUNDEMAIL: emptyTrash() Could not delete message [ {$uid} ] from [ {$this->mailbox} ]. IMAP_ERROR [ {$lastError} ]");
  384. }
  385. }
  386. // remove local cache file
  387. $q = "DELETE FROM email_cache WHERE mbox = '{$this->mailbox}' AND ie_id = '{$this->id}'";
  388. $r = $this->db->query($q);
  389. }
  390. /**
  391. * Fetches a timestamp
  392. */
  393. function getCacheTimestamp($mbox) {
  394. $key = $this->db->quote("{$this->id}_{$mbox}");
  395. $q = "SELECT ie_timestamp FROM inbound_email_cache_ts WHERE id = '{$key}'";
  396. $r = $this->db->query($q);
  397. $a = $this->db->fetchByAssoc($r);
  398. if(empty($a)) {
  399. return -1;
  400. }
  401. return $a['ie_timestamp'];
  402. }
  403. /**
  404. * sets the cache timestamp
  405. * @param string mbox
  406. */
  407. function setCacheTimestamp($mbox) {
  408. $key = $this->db->quote("{$this->id}_{$mbox}");
  409. $ts = mktime();
  410. $tsOld = $this->getCacheTimestamp($mbox);
  411. if($tsOld < 0) {
  412. $q = "INSERT INTO inbound_email_cache_ts (id, ie_timestamp) VALUES ('{$key}', {$ts})";
  413. } else {
  414. $q = "UPDATE inbound_email_cache_ts SET ie_timestamp = {$ts} WHERE id = '{$key}'";
  415. }
  416. $r = $this->db->query($q, true);
  417. $GLOBALS['log']->info("INBOUNDEMAIL-CACHE: setting timestamp query [ {$q} ]");
  418. }
  419. /**
  420. * Gets a count of all rows that are flagged seen = 0
  421. * @param string $mbox
  422. * @return int
  423. */
  424. function getCacheUnreadCount($mbox) {
  425. $q = "SELECT count(*) c FROM email_cache WHERE mbox = '{$mbox}' AND seen = 0 AND ie_id = '{$this->id}'";
  426. $r = $this->db->query($q);
  427. $a = $this->db->fetchByAssoc($r);
  428. return $a['c'];
  429. }
  430. /**
  431. * Returns total number of emails for a mailbox
  432. * @param string mbox
  433. * @return int
  434. */
  435. function getCacheCount($mbox) {
  436. $q = "SELECT count(*) c FROM email_cache WHERE mbox = '{$mbox}' AND ie_id = '{$this->id}'";
  437. $r = $this->db->query($q);
  438. $a = $this->db->fetchByAssoc($r);
  439. return $a['c'];
  440. }
  441. function getCacheUnread($mbox) {
  442. $q = "SELECT count(*) c FROM email_cache WHERE mbox = '{$mbox}' AND ie_id = '{$this->id}' AND seen = '0'";
  443. $r = $this->db->query($q);
  444. $a = $this->db->fetchByAssoc($r);
  445. return $a['c'];
  446. }
  447. /**
  448. * Deletes all rows for a given instance
  449. */
  450. function deleteCache() {
  451. $q = "DELETE FROM email_cache WHERE ie_id = '{$this->id}'";
  452. $GLOBALS['log']->info("INBOUNDEMAIL: deleting cache using query [ {$q} ]");
  453. $r = $this->db->query($q);
  454. }
  455. /**
  456. * Deletes all the pop3 data which has been deleted from server
  457. */
  458. function deletePop3Cache() {
  459. global $sugar_config;
  460. $UIDLs = $this->pop3_getUIDL();
  461. $cacheUIDLs = $this->pop3_getCacheUidls();
  462. foreach($cacheUIDLs as $msgNo => $msgId) {
  463. if (!in_array($msgId, $UIDLs)) {
  464. $md5msgIds = md5($msgId);
  465. $file = "{$this->EmailCachePath}/{$this->id}/messages/INBOX{$md5msgIds}.PHP";
  466. $GLOBALS['log']->debug("INBOUNDEMAIL: deleting file [ {$file} ] ");
  467. if(file_exists($file)) {
  468. if(!unlink($file)) {
  469. $GLOBALS['log']->debug("INBOUNDEMAIL: Could not delete [ {$file} ] ");
  470. } // if
  471. } // if
  472. $q = "DELETE from email_cache where imap_uid = {$msgNo} AND msgno = {$msgNo} AND ie_id = '{$this->id}' AND message_id = '{$msgId}'";
  473. $r = $this->db->query($q);
  474. } // if
  475. } // for
  476. } // fn
  477. /**
  478. * Retrieves cached headers
  479. * @return array
  480. */
  481. function getCacheValueForUIDs($mbox, $UIDs) {
  482. if (!is_array($UIDs) || empty($UIDs)) {
  483. return array();
  484. }
  485. $q = "SELECT * FROM email_cache WHERE ie_id = '{$this->id}' AND mbox = '{$mbox}' AND ";
  486. $startIndex = 0;
  487. $endIndex = 5;
  488. $slicedArray = array_slice($UIDs, $startIndex ,$endIndex);
  489. $columnName = ($this->isPop3Protocol() ? "message_id" : "imap_uid");
  490. $ret = array(
  491. 'timestamp' => $this->getCacheTimestamp($mbox),
  492. 'uids' => array(),
  493. 'retArr' => array(),
  494. );
  495. while (!empty($slicedArray)) {
  496. $messageIdString = implode(',', $slicedArray);
  497. $GLOBALS['log']->debug("sliced array = {$messageIdString}");
  498. $extraWhere = "{$columnName} IN (";
  499. $i = 0;
  500. foreach($slicedArray as $UID) {
  501. if($i != 0) {
  502. $extraWhere = $extraWhere . ",";
  503. } // if
  504. $i++;
  505. $extraWhere = "{$extraWhere} '{$UID}'";
  506. } // foreach
  507. $newQuery = $q . $extraWhere . ")";
  508. $r = $this->db->query($newQuery);
  509. while($a = $this->db->fetchByAssoc($r)) {
  510. if (isset($a['uid'])) {
  511. if ($this->isPop3Protocol()) {
  512. $ret['uids'][] = $a['message_id'];
  513. } else {
  514. $ret['uids'][] = $a['uid'];
  515. }
  516. }
  517. $overview = new Overview();
  518. foreach($a as $k => $v) {
  519. $k=strtolower($k);
  520. switch($k) {
  521. case "imap_uid":
  522. $overview->imap_uid = $v;
  523. if ($this->isPop3Protocol()) {
  524. $overview->uid = $a['message_id'];
  525. } else {
  526. $overview->uid = $v;
  527. }
  528. break;
  529. case "toaddr":
  530. $overview->to = from_html($v);
  531. break;
  532. case "fromaddr":
  533. $overview->from = from_html($v);
  534. break;
  535. case "mailsize":
  536. $overview->size = $v;
  537. break;
  538. case "senddate":
  539. $overview->date = $v;
  540. break;
  541. default:
  542. $overview->$k = from_html($v);
  543. break;
  544. } // switch
  545. } // foreach
  546. $ret['retArr'][] = $overview;
  547. } // while
  548. $startIndex = $startIndex + $endIndex;
  549. $slicedArray = array_slice($UIDs, $startIndex ,$endIndex);
  550. $messageIdString = implode(',', $slicedArray);
  551. $GLOBALS['log']->debug("sliced array = {$messageIdString}");
  552. } // while
  553. return $ret;
  554. }
  555. /**
  556. * Retrieves cached headers
  557. * @return array
  558. */
  559. function getCacheValue($mbox, $limit = 20, $page = 1, $sort='', $direction='') {
  560. // try optimizing this call as we don't want repeat queries
  561. if(!empty($this->currentCache)) {
  562. return $this->currentCache;
  563. }
  564. $sort = (empty($sort)) ? $this->defaultSort : $sort;
  565. $direction = (empty($direction)) ? $this->defaultDirection : $direction;
  566. $order = " ORDER BY {$this->hrSortLocal[$sort]} {$direction}";
  567. $q = "SELECT * FROM email_cache WHERE ie_id = '{$this->id}' AND mbox = '{$mbox}' {$order}";
  568. if(!empty($limit)) {
  569. $start = ( $page - 1 ) * $limit;
  570. $r = $this->db->limitQuery($q, $start, $limit);
  571. } else {
  572. $r = $this->db->query($q);
  573. }
  574. $ret = array(
  575. 'timestamp' => $this->getCacheTimestamp($mbox),
  576. 'uids' => array(),
  577. 'retArr' => array(),
  578. );
  579. while($a = $this->db->fetchByAssoc($r)) {
  580. if (isset($a['uid'])) {
  581. if ($this->isPop3Protocol()) {
  582. $ret['uids'][] = $a['message_id'];
  583. } else {
  584. $ret['uids'][] = $a['uid'];
  585. }
  586. }
  587. $overview = new Overview();
  588. foreach($a as $k => $v) {
  589. $k=strtolower($k);
  590. switch($k) {
  591. case "imap_uid":
  592. $overview->imap_uid = $v;
  593. if ($this->isPop3Protocol()) {
  594. $overview->uid = $a['message_id'];
  595. } else {
  596. $overview->uid = $v;
  597. }
  598. break;
  599. case "toaddr":
  600. $overview->to = from_html($v);
  601. break;
  602. case "fromaddr":
  603. $overview->from = from_html($v);
  604. break;
  605. case "mailsize":
  606. $overview->size = $v;
  607. break;
  608. case "senddate":
  609. $overview->date = $v;
  610. break;
  611. default:
  612. $overview->$k = from_html($v);
  613. break;
  614. }
  615. }
  616. $ret['retArr'][] = $overview;
  617. }
  618. $this->currentCache = $ret;
  619. return $ret;
  620. }
  621. /**
  622. * Sets cache values
  623. */
  624. function setCacheValue($mbox, $insert, $update=array(), $remove=array()) {
  625. if(empty($mbox)) {
  626. return;
  627. }
  628. global $timedate;
  629. // reset in-memory cache
  630. $this->currentCache = null;
  631. $table = 'email_cache';
  632. $where = "WHERE ie_id = '{$this->id}' AND mbox = '{$mbox}'";
  633. // handle removed rows
  634. if(!empty($remove)) {
  635. $removeIds = '';
  636. foreach($remove as $overview) {
  637. if(!empty($removeIds)) {
  638. $removeIds .= ",";
  639. }
  640. $removeIds .= "'{$overview->imap_uid}'";
  641. }
  642. $q = "DELETE FROM {$table} {$where} AND imap_uid IN ({$removeIds})";
  643. $GLOBALS['log']->info("INBOUNDEMAIL-CACHE: delete query [ {$q} ]");
  644. $r = $this->db->query($q, true, $q);
  645. }
  646. // handle insert rows
  647. if(!empty($insert)) {
  648. $q = "SELECT imap_uid FROM {$table} {$where}";
  649. $GLOBALS['log']->info("INBOUNDEMAIL-CACHE: filter UIDs query [ {$q} ]");
  650. $r = $this->db->query($q);
  651. $uids = array();
  652. while($a = $this->db->fetchByAssoc($r)) {
  653. $uids[] = $a['imap_uid'];
  654. }
  655. $count = count($uids);
  656. $GLOBALS['log']->info("INBOUNDEMAIL-CACHE: found [ {$count} ] UIDs to filter against");
  657. $tmp = '';
  658. foreach($uids as $uid) {
  659. if(!empty($tmp))
  660. $tmp .= ", ";
  661. $tmp .= "{$uid}";
  662. }
  663. $GLOBALS['log']->info("INBOUNDEMAIL-CACHE: filter UIDs: [ {$tmp} ]");
  664. $cols = "";
  665. foreach($this->overview->fieldDefs as $colDef) {
  666. if(!empty($cols))
  667. $cols .= ",";
  668. $cols .= "{$colDef['name']}";
  669. }
  670. foreach($insert as $overview) {
  671. if(in_array($overview->imap_uid, $uids))
  672. {
  673. // fixing bug #49543: setting 'mbox' property for the following updating of other items in this box
  674. if (!isset($overview->mbox))
  675. {
  676. $overview->mbox = $mbox;
  677. }
  678. $update[] = $overview;
  679. continue;
  680. }
  681. $values = '';
  682. foreach($this->overview->fieldDefs as $colDef) {
  683. if(!empty($values)) {
  684. $values .= ", ";
  685. }
  686. // trim values for Oracle/MSSql
  687. if( isset($colDef['len']) && !empty($colDef['len']) &&
  688. isset($colDef['type']) && !empty($colDef['type']) &&
  689. $colDef['type'] == 'varchar'
  690. )
  691. {
  692. if (isset($overview->$colDef['name']))
  693. {
  694. $overview->$colDef['name'] = substr($overview->$colDef['name'], 0, $colDef['len']);
  695. }
  696. }
  697. switch($colDef['name']) {
  698. case "imap_uid":
  699. if(isset($overview->uid) && !empty($overview->uid)) {
  700. $this->imap_uid = $overview->uid;
  701. }
  702. $values .= "'{$this->imap_uid}'";
  703. break;
  704. case "ie_id":
  705. $values .= "'{$this->id}'";
  706. break;
  707. case "toaddr":
  708. $values .= $this->db->quoted($overview->to);
  709. break;
  710. case "fromaddr":
  711. $values .= $this->db->quoted($overview->from);
  712. break;
  713. case "message_id" :
  714. $values .= $this->db->quoted($overview->message_id);
  715. break;
  716. case "mailsize":
  717. $values .= $overview->size;
  718. break;
  719. case "senddate":
  720. $conv=$timedate->fromString($overview->date);
  721. if (!empty($conv)) {
  722. $values .= $this->db->quoted($conv->asDb());
  723. } else {
  724. $values .= "NULL";
  725. }
  726. break;
  727. case "mbox":
  728. $values .= "'{$mbox}'";
  729. break;
  730. default:
  731. $overview->$colDef['name'] = SugarCleaner::cleanHtml(from_html($overview->$colDef['name']));
  732. $values .= $this->db->quoted($overview->$colDef['name']);
  733. break;
  734. }
  735. }
  736. $q = "INSERT INTO {$table} ({$cols}) VALUES ({$values})";
  737. $GLOBALS['log']->info("INBOUNDEMAIL-CACHE: insert query [ {$q} ]");
  738. $r = $this->db->query($q, true, $q);
  739. }
  740. }
  741. // handle update rows
  742. if(!empty($update)) {
  743. $cols = "";
  744. foreach($this->overview->fieldDefs as $colDef) {
  745. if(!empty($cols))
  746. $cols .= ",";
  747. $cols .= "{$colDef['name']}";
  748. }
  749. foreach($update as $overview) {
  750. $q = "UPDATE {$table} SET ";
  751. $set = '';
  752. foreach($this->overview->fieldDefs as $colDef) {
  753. switch($colDef['name']) {
  754. case "toaddr":
  755. case "fromaddr":
  756. case "mailsize":
  757. case "senddate":
  758. case "mbox":
  759. case "ie_id":
  760. break;
  761. default:
  762. if(!empty($set))
  763. {
  764. $set .= ",";
  765. }
  766. $value = '';
  767. if (isset($overview->$colDef['name']))
  768. {
  769. $value = $this->db->quoted($overview->$colDef['name']);
  770. }
  771. else
  772. {
  773. $value = $this->db->quoted($value);
  774. }
  775. $set .= "{$colDef['name']} = " . $value;
  776. break;
  777. }
  778. }
  779. $q .= $set . " WHERE ie_id = '{$this->id}' AND mbox = '{$overview->mbox}' AND imap_uid = '{$overview->imap_uid}'";
  780. $GLOBALS['log']->info("INBOUNDEMAIL-CACHE: update query [ {$q} ]");
  781. $r = $this->db->query($q, true, $q);
  782. }
  783. }
  784. }
  785. /**
  786. * Opens a socket connection to the pop3 server
  787. * @return bool
  788. */
  789. function pop3_open() {
  790. if(!is_resource($this->pop3socket)) {
  791. $GLOBALS['log']->info("*** INBOUNDEMAIL: opening socket connection");
  792. $exServ = explode('::', $this->service);
  793. $socket = ($exServ[2] == 'ssl') ? "ssl://" : "tcp://";
  794. $socket .= $this->server_url;
  795. $this->pop3socket = fsockopen($socket, $this->port);
  796. } else {
  797. $GLOBALS['log']->info("*** INBOUNDEMAIL: REUSING socket connection");
  798. return true;
  799. }
  800. if(!is_resource($this->pop3socket)) {
  801. $GLOBALS['log']->debug("*** INBOUNDEMAIL: unable to open socket connection");
  802. return false;
  803. }
  804. // clear buffer
  805. $ret = trim(fgets($this->pop3socket, 1024));
  806. $GLOBALS['log']->info("*** INBOUNDEMAIL: got socket connection [ {$ret} ]");
  807. return true;
  808. }
  809. /**
  810. * Closes connections and runs clean-up routines
  811. */
  812. function pop3_cleanUp() {
  813. $GLOBALS['log']->info("*** INBOUNDEMAIL: cleaning up socket connection");
  814. fputs($this->pop3socket, "QUIT\r\n");
  815. $buf = fgets($this->pop3socket, 1024);
  816. fclose($this->pop3socket);
  817. }
  818. /**
  819. * sends a command down to the POP3 server
  820. * @param string command
  821. * @param string args
  822. * @param bool return
  823. * @return string
  824. */
  825. function pop3_sendCommand($command, $args='', $return=true) {
  826. $command .= " {$args}";
  827. $command = trim($command);
  828. $GLOBALS['log']->info("*** INBOUNDEMAIL: pop3_sendCommand() SEND [ {$command} ]");
  829. $command .= "\r\n";
  830. fputs($this->pop3socket, $command);
  831. if($return) {
  832. $ret = trim(fgets($this->pop3socket, 1024));
  833. $GLOBALS['log']->info("*** INBOUNDEMAIL: pop3_sendCommand() RECEIVE [ {$ret} ]");
  834. return $ret;
  835. }
  836. }
  837. function getPop3NewMessagesToDownload() {
  838. $pop3UIDL = $this->pop3_getUIDL();
  839. $cacheUIDLs = $this->pop3_getCacheUidls();
  840. // new email cache values we should deal with
  841. $diff = array_diff_assoc($pop3UIDL, $cacheUIDLs);
  842. // this is msgNo to UIDL array
  843. $diff = $this->pop3_shiftCache($diff, $cacheUIDLs);
  844. // get all the keys which are msgnos;
  845. return array_keys($diff);
  846. }
  847. function getPop3NewMessagesToDownloadForCron() {
  848. $pop3UIDL = $this->pop3_getUIDL();
  849. $cacheUIDLs = $this->pop3_getCacheUidls();
  850. // new email cache values we should deal with
  851. $diff = array_diff_assoc($pop3UIDL, $cacheUIDLs);
  852. // this is msgNo to UIDL array
  853. $diff = $this->pop3_shiftCache($diff, $cacheUIDLs);
  854. // insert data into email_cache
  855. if ($this->groupfolder_id != null && $this->groupfolder_id != "" && $this->isPop3Protocol()) {
  856. $searchResults = array_keys($diff);
  857. $concatResults = implode(",", $searchResults);
  858. if ($this->connectMailserver() == 'true') {
  859. $fetchedOverviews = imap_fetch_overview($this->conn, $concatResults);
  860. // clean up cache entry
  861. foreach($fetchedOverviews as $k => $overview) {
  862. $overview->message_id = trim($diff[$overview->msgno]);
  863. $fetchedOverviews[$k] = $overview;
  864. }
  865. $this->updateOverviewCacheFile($fetchedOverviews);
  866. }
  867. } // if
  868. return $diff;
  869. }
  870. /**
  871. * This method returns all the UIDL for this account. This should be called if the protocol is pop3
  872. * @return array od messageno to UIDL array
  873. */
  874. function pop3_getUIDL() {
  875. $UIDLs = array();
  876. if($this->pop3_open()) {
  877. // authenticate
  878. $this->pop3_sendCommand("USER", $this->email_user);
  879. $this->pop3_sendCommand("PASS", $this->email_password);
  880. // get UIDLs
  881. $this->pop3_sendCommand("UIDL", '', false); // leave socket buffer alone until the while()
  882. fgets($this->pop3socket, 1024); // handle "OK+";
  883. $UIDLs = array();
  884. $buf = '!';
  885. if(is_resource($this->pop3socket)) {
  886. while(!feof($this->pop3socket)) {
  887. $buf = fgets($this->pop3socket, 1024); // 8kb max buffer - shouldn't be more than 80 chars via pop3...
  888. //_pp(trim($buf));
  889. if(trim($buf) == '.') {
  890. $GLOBALS['log']->debug("*** GOT '.'");
  891. break;
  892. }
  893. // format is [msgNo] [UIDL]
  894. $exUidl = explode(" ", $buf);
  895. $UIDLs[$exUidl[0]] = trim($exUidl[1]);
  896. } // while
  897. } // if
  898. $this->pop3_cleanUp();
  899. } // if
  900. return $UIDLs;
  901. } // fn
  902. /**
  903. * Special handler for POP3 boxes. Standard IMAP commands are useless.
  904. * This will fetch only partial emails for POP3 and hence needs to be call again and again based on status it returns
  905. */
  906. function pop3_checkPartialEmail($synch = false) {
  907. require_once('include/utils/array_utils.php');
  908. global $current_user;
  909. global $sugar_config;
  910. $cacheDataExists = false;
  911. $diff = array();
  912. $results = array();
  913. $cacheFilePath = clean_path("{$this->EmailCachePath}/{$this->id}/folders/MsgNOToUIDLData.php");
  914. if(file_exists($cacheFilePath)) {
  915. $cacheDataExists = true;
  916. if($fh = @fopen($cacheFilePath, "rb")) {
  917. $data = "";
  918. $chunksize = 1*(1024*1024); // how many bytes per chunk
  919. while(!feof($fh)) {
  920. $buf = fgets($fh, $chunksize); // 8kb max buffer - shouldn't be more than 80 chars via pop3...
  921. $data = $data . $buf;
  922. flush();
  923. } // while
  924. fclose($fh);
  925. $diff = unserialize($data);
  926. if (!empty($diff)) {
  927. if (count($diff)> 50) {
  928. $newDiff = array_slice($diff, 50, count($diff), true);
  929. } else {
  930. $newDiff=array();
  931. }
  932. $results = array_slice(array_keys($diff), 0 ,50);
  933. $data = serialize($newDiff);
  934. if($fh = @fopen($cacheFilePath, "w")) {
  935. fputs($fh, $data);
  936. fclose($fh);
  937. } // if
  938. }
  939. } // if
  940. } // if
  941. if (!$cacheDataExists) {
  942. if ($synch) {
  943. $this->deletePop3Cache();
  944. }
  945. $UIDLs = $this->pop3_getUIDL();
  946. if(count($UIDLs) > 0) {
  947. // get cached UIDLs
  948. $cacheUIDLs = $this->pop3_getCacheUidls();
  949. // new email cache values we should deal with
  950. $diff = array_diff_assoc($UIDLs, $cacheUIDLs);
  951. $diff = $this->pop3_shiftCache($diff, $cacheUIDLs);
  952. require_once('modules/Emails/EmailUI.php');
  953. EmailUI::preflightEmailCache("{$this->EmailCachePath}/{$this->id}");
  954. if (count($diff)> 50) {
  955. $newDiff = array_slice($diff, 50, count($diff), true);
  956. } else {
  957. $newDiff=array();
  958. }
  959. $results = array_slice(array_keys($diff), 0 ,50);
  960. $data = serialize($newDiff);
  961. if($fh = @fopen($cacheFilePath, "w")) {
  962. fputs($fh, $data);
  963. fclose($fh);
  964. } // if
  965. } else {
  966. $GLOBALS['log']->debug("*** INBOUNDEMAIL: could not open socket connection to POP3 server");
  967. return "could not open socket connection to POP3 server";
  968. } // else
  969. } // if
  970. // build up msgNo request
  971. if(count($diff) > 0) {
  972. // remove dirty cache entries
  973. $startingNo = 0;
  974. if (isset($_REQUEST['currentCount']) && $_REQUEST['currentCount'] > -1) {
  975. $startingNo = $_REQUEST['currentCount'];
  976. }
  977. $this->mailbox = 'INBOX';
  978. $this->connectMailserver();
  979. //$searchResults = array_keys($diff);
  980. //$fetchedOverviews = array();
  981. //$chunkArraySerachResults = array_chunk($searchResults, 50);
  982. $concatResults = implode(",", $results);
  983. $GLOBALS['log']->info('$$$$ '.$concatResults);
  984. $GLOBALS['log']->info("[EMAIL] Start POP3 fetch overview on mailbox [{$this->mailbox}] for user [{$current_user->user_name}] on 50 data");
  985. $fetchedOverviews = imap_fetch_overview($this->conn, $concatResults);
  986. $GLOBALS['log']->info("[EMAIL] End POP3 fetch overview on mailbox [{$this->mailbox}] for user [{$current_user->user_name}] on "
  987. . sizeof($fetchedOverviews) . " data");
  988. // clean up cache entry
  989. foreach($fetchedOverviews as $k => $overview) {
  990. $overview->message_id = trim($diff[$overview->msgno]);
  991. $fetchedOverviews[$k] = $overview;
  992. }
  993. $GLOBALS['log']->info("[EMAIL] Start updating overview cache for pop3 mailbox [{$this->mailbox}] for user [{$current_user->user_name}]");
  994. $this->updateOverviewCacheFile($fetchedOverviews);
  995. $GLOBALS['log']->info("[EMAIL] Start updating overview cache for pop3 mailbox [{$this->mailbox}] for user [{$current_user->user_name}]");
  996. return array('status' => "In Progress", 'mbox' => $this->mailbox, 'count'=> (count($results) + $startingNo), 'totalcount' => count($diff), 'ieid' => $this->id);
  997. } // if
  998. unlink($cacheFilePath);
  999. return array('status' => "done");
  1000. }
  1001. /**
  1002. * Special handler for POP3 boxes. Standard IMAP commands are useless.
  1003. */
  1004. function pop3_checkEmail() {
  1005. if($this->pop3_open()) {
  1006. // authenticate
  1007. $this->pop3_sendCommand("USER", $this->email_user);
  1008. $this->pop3_sendCommand("PASS", $this->email_password);
  1009. // get UIDLs
  1010. $this->pop3_sendCommand("UIDL", '', false); // leave socket buffer alone until the while()
  1011. fgets($this->pop3socket, 1024); // handle "OK+";
  1012. $UIDLs = array();
  1013. $buf = '!';
  1014. if(is_resource($this->pop3socket)) {
  1015. while(!feof($this->pop3socket)) {
  1016. $buf = fgets($this->pop3socket, 1024); // 8kb max buffer - shouldn't be more than 80 chars via pop3...
  1017. //_pp(trim($buf));
  1018. if(trim($buf) == '.') {
  1019. $GLOBALS['log']->debug("*** GOT '.'");
  1020. break;
  1021. }
  1022. // format is [msgNo] [UIDL]
  1023. $exUidl = explode(" ", $buf);
  1024. $UIDLs[$exUidl[0]] = trim($exUidl[1]);
  1025. }
  1026. }
  1027. $this->pop3_cleanUp();
  1028. // get cached UIDLs
  1029. $cacheUIDLs = $this->pop3_getCacheUidls();
  1030. // _pp($UIDLs);_pp($cacheUIDLs);
  1031. // new email cache values we should deal with
  1032. $diff = array_diff_assoc($UIDLs, $cacheUIDLs);
  1033. // remove dirty cache entries
  1034. $diff = $this->pop3_shiftCache($diff, $cacheUIDLs);
  1035. // build up msgNo request
  1036. if(!empty($diff)) {
  1037. $this->mailbox = 'INBOX';
  1038. $this->connectMailserver();
  1039. $searchResults = array_keys($diff);
  1040. $concatResults = implode(",", $searchResults);
  1041. $fetchedOverviews = imap_fetch_overview($this->conn, $concatResults);
  1042. // clean up cache entry
  1043. foreach($fetchedOverviews as $k => $overview) {
  1044. $overview->message_id = trim($diff[$overview->msgno]);
  1045. $fetchedOverviews[$k] = $overview;
  1046. }
  1047. $this->updateOverviewCacheFile($fetchedOverviews);
  1048. }
  1049. } else {
  1050. $GLOBALS['log']->debug("*** INBOUNDEMAIL: could not open socket connection to POP3 server");
  1051. return false;
  1052. }
  1053. }
  1054. /**
  1055. * Iterates through msgno and message_id to remove dirty cache entries
  1056. * @param array diff
  1057. */
  1058. function pop3_shiftCache($diff, $cacheUIDLs) {
  1059. $msgNos = "";
  1060. $msgIds = "";
  1061. $newArray = array();
  1062. foreach($diff as $msgNo => $msgId) {
  1063. if (in_array($msgId, $cacheUIDLs)) {
  1064. $q1 = "UPDATE email_cache SET imap_uid = {$msgNo}, msgno = {$msgNo} WHERE ie_id = '{$this->id}' AND message_id = '{$msgId}'";
  1065. $this->db->query($q1);
  1066. } else {
  1067. $newArray[$msgNo] = $msgId;
  1068. }
  1069. }
  1070. return $newArray;
  1071. /*
  1072. foreach($diff as $msgNo => $msgId) {
  1073. if(!empty($msgNos)) {
  1074. $msgNos .= ", ";
  1075. }
  1076. if(!empty($msgIds)) {
  1077. $msgIds .= ", ";
  1078. }
  1079. $msgNos .= $msgNo;
  1080. $msgIds .= "'{$msgId}'";
  1081. }
  1082. if(!empty($msgNos)) {
  1083. $q1 = "DELETE FROM email_cache WHERE ie_id = '{$this->id}' AND msgno IN ({$msgNos})";
  1084. $this->db->query($q1);
  1085. }
  1086. if(!empty($msgIds)) {
  1087. $q2 = "DELETE FROM email_cache WHERE ie_id = '{$this->id}' AND message_id IN ({$msgIds})";
  1088. $this->db->query($q2);
  1089. }
  1090. */
  1091. }
  1092. /**
  1093. * retrieves cached uidl values.
  1094. * When dealing with POP3 accounts, the message_id column in email_cache will contain the UIDL.
  1095. * @return array
  1096. */
  1097. function pop3_getCacheUidls() {
  1098. $q = "SELECT msgno, message_id FROM email_cache WHERE ie_id = '{$this->id}'";
  1099. $r = $this->db->query($q);
  1100. $ret = array();
  1101. while($a = $this->db->fetchByAssoc($r)) {
  1102. $ret[$a['msgno']] = $a['message_id'];
  1103. }
  1104. return $ret;
  1105. }
  1106. /**
  1107. * This function is used by cron job for group mailbox without group folder
  1108. * @param string $msgno for pop
  1109. * @param string $uid for imap
  1110. */
  1111. function getMessagesInEmailCache($msgno, $uid) {
  1112. $fetchedOverviews = array();
  1113. if ($this->isPop3Protocol()) {
  1114. $fetchedOverviews = imap_fetch_overview($this->conn, $msgno);
  1115. foreach($fetchedOverviews as $k => $overview) {
  1116. $overview->message_id = $uid;
  1117. $fetchedOverviews[$k] = $overview;
  1118. }
  1119. } else {
  1120. $fetchedOverviews = imap_fetch_overview($this->conn, $uid, FT_UID);
  1121. } // else
  1122. $this->updateOverviewCacheFile($fetchedOverviews);
  1123. } // fn
  1124. /**
  1125. * Checks email (local caching too) for one mailbox
  1126. * @param string $mailbox IMAP Mailbox path
  1127. * @param bool $prefetch Flag to prefetch email body on check
  1128. */
  1129. function checkEmailOneMailbox($mailbox, $prefetch=true, $synchronize=false) {
  1130. global $sugar_config;
  1131. global $current_user;
  1132. global $app_strings;
  1133. $result = 1;
  1134. $GLOBALS['log']->info("INBOUNDEMAIL: checking mailbox [ {$mailbox} ]");
  1135. $this->mailbox = $mailbox;
  1136. $this->connectMailserver();
  1137. $checkTime = '';
  1138. $shouldProcessRules = true;
  1139. $timestamp = $this->getCacheTimestamp($mailbox);
  1140. if($timestamp > 0) {
  1141. $checkTime = date('r', $timestamp);
  1142. }
  1143. /* first time through, process ALL emails */
  1144. if(empty($checkTime) || $synchronize) {
  1145. // do not process rules for the first time or sunchronize
  1146. $shouldProcessRules = false;
  1147. $criteria = "ALL UNDELETED";
  1148. $prefetch = false; // do NOT prefetch emails on a brand new account - timeouts happen.
  1149. $GLOBALS['log']->info("INBOUNDEMAIL: new account detected - not prefetching email bodies.");
  1150. } else {
  1151. $criteria = "SINCE \"{$checkTime}\" UNDELETED"; // not using UNSEEN
  1152. }
  1153. $this->setCacheTimestamp($mailbox);
  1154. $GLOBALS['log']->info("[EMAIL] Performing IMAP search using criteria [{$criteria}] on mailbox [{$mailbox}] for user [{$current_user->user_name}]");
  1155. $searchResults = imap_search($this->conn, $criteria, SE_UID);
  1156. $GLOBALS['log']->info("[EMAIL] Done IMAP search on mailbox [{$mailbox}] for user [{$current_user->user_name}]. Result count = ".count($searchResults));
  1157. if(!empty($searchResults)) {
  1158. $concatResults = implode(",", $searchResults);
  1159. $GLOBALS['log']->info("[EMAIL] Start IMAP fetch overview on mailbox [{$mailbox}] for user [{$current_user->user_name}]");
  1160. $fetchedOverview = imap_fetch_overview($this->conn, $concatResults, FT_UID);
  1161. $GLOBALS['log']->info("[EMAIL] Done IMAP fetch overview on mailbox [{$mailbox}] for user [{$current_user->user_name}]");
  1162. $GLOBALS['log']->info("[EMAIL] Start updating overview cache for mailbox [{$mailbox}] for user [{$current_user->user_name}]");
  1163. $this->updateOverviewCacheFile($fetchedOverview);
  1164. $GLOBALS['log']->info("[EMAIL] Done updating overview cache for mailbox [{$mailbox}] for user [{$current_user->user_name}]");
  1165. // prefetch emails
  1166. if($prefetch == true) {
  1167. $GLOBALS['log']->info("[EMAIL] Start fetching emails for mailbox [{$mailbox}] for user [{$current_user->user_name}]");
  1168. if(!$this->fetchCheckedEmails($fetchedOverview))
  1169. {
  1170. $result = 0;
  1171. }
  1172. $GLOBALS['log']->info("[EMAIL] Done fetching emails for mailbox [{$mailbox}] for user [{$current_user->user_name}]");
  1173. }
  1174. } else {
  1175. $GLOBALS['log']->info("INBOUNDEMAIL: no results for mailbox [ {$mailbox} ]");
  1176. $result = 1;
  1177. }
  1178. /**
  1179. * To handle the use case where an external client is also connected, deleting emails, we need to clear our
  1180. * local cache of all emails with the "DELETED" flag
  1181. */
  1182. $criteria = 'DELETED';
  1183. $criteria .= (!empty($checkTime)) ? " SINCE \"{$checkTime}\"" : "";
  1184. $GLOBALS['log']->info("INBOUNDEMAIL: checking for deleted emails using [ {$criteria} ]");
  1185. $trashFolder = $this->get_stored_options("trashFolder");
  1186. if (empty($trashFolder)) {
  1187. $trashFolder = "INBOX.Trash";
  1188. }
  1189. if($this->mailbox != $trashFolder) {
  1190. $searchResults = imap_search($this->conn, $criteria, SE_UID);
  1191. if(!empty($searchResults)) {
  1192. $uids = implode($app_strings['LBL_EMAIL_DELIMITER'], $searchResults);
  1193. $GLOBALS['log']->info("INBOUNDEMAIL: removing UIDs found deleted [ {$uids} ]");
  1194. $this->getOverviewsFromCacheFile($uids, $mailbox, true);
  1195. }
  1196. }
  1197. return $result;
  1198. }
  1199. /**
  1200. * Checks email (local caching too) for one mailbox
  1201. * @param string $mailbox IMAP Mailbox path
  1202. * @param bool $prefetch Flag to prefetch email body on check
  1203. */
  1204. function checkEmailOneMailboxPartial($mailbox, $prefetch=true, $synchronize=false, $start = 0, $max = -1) {
  1205. global $sugar_config;
  1206. global $current_user;
  1207. global $app_strings;
  1208. $GLOBALS['log']->info("INBOUNDEMAIL: checking mailbox [ {$mailbox} ]");
  1209. $this->mailbox = $mailbox;
  1210. $this->connectMailserver();
  1211. $checkTime = '';
  1212. $shouldProcessRules = true;
  1213. $timestamp = $this->getCacheTimestamp($mailbox);
  1214. if($timestamp > 0) {
  1215. $checkTime = date('r', $timestamp);
  1216. }
  1217. /* first time through, process ALL emails */
  1218. if(empty($checkTime) || $synchronize) {
  1219. // do not process rules for the first time or sunchronize
  1220. $shouldProcessRules = false;
  1221. $criteria = "ALL UNDELETED";
  1222. $prefetch = false; // do NOT prefetch emails on a brand new account - timeouts happen.
  1223. $GLOBALS['log']->info("INBOUNDEMAIL: new account detected - not prefetching email bodies.");
  1224. } else {
  1225. $criteria = "SINCE \"{$checkTime}\" UNDELETED"; // not using UNSEEN
  1226. }
  1227. $this->setCacheTimestamp($mailbox);
  1228. $GLOBALS['log']->info("[EMAIL] Performing IMAP search using criteria [{$criteria}] on mailbox [{$mailbox}] for user [{$current_user->user_name}]");
  1229. $searchResults = $this->getCachedIMAPSearch($criteria);
  1230. if(!empty($searchResults)) {
  1231. $total = sizeof($searchResults);
  1232. $searchResults = array_slice($searchResults, $start, $max);
  1233. $GLOBALS['log']->info("INBOUNDEMAIL: there are $total messages in [{$mailbox}], we are on $start");
  1234. $GLOBALS['log']->info("INBOUNDEMAIL: getting the next " . sizeof($searchResults) . " messages");
  1235. $concatResults = implode(",", $searchResults);
  1236. $GLOBALS['log']->info("INBOUNDEMAIL: Start IMAP fetch overview on mailbox [{$mailbox}] for user [{$current_user->user_name}]");
  1237. $fetchedOverview = imap_fetch_overview($this->conn, $concatResults, FT_UID);
  1238. $GLOBALS['log']->info("INBOUNDEMAIL: Done IMAP fetch overview on mailbox [{$mailbox}] for user [{$current_user->user_name}]");
  1239. $GLOBALS['log']->info("INBOUNDEMAIL: Start updating overview cache for mailbox [{$mailbox}] for user [{$current_user->user_name}]");
  1240. $this->updateOverviewCacheFile($fetchedOverview);
  1241. $GLOBALS['log']->info("INBOUNDEMAIL: Done updating overview cache for mailbox [{$mailbox}] for user [{$current_user->user_name}]");
  1242. // prefetch emails
  1243. if($prefetch == true) {
  1244. $GLOBALS['log']->info("INBOUNDEMAIL: Start fetching emails for mailbox [{$mailbox}] for user [{$current_user->user_name}]");
  1245. $this->fetchCheckedEmails($fetchedOverview);
  1246. $GLOBALS['log']->info("INBOUNDEMAIL: Done fetching emails for mailbox [{$mailbox}] for user [{$current_user->user_name}]");
  1247. }
  1248. $status = ($total > $start + sizeof($searchResults)) ? 'continue' : 'done';
  1249. $ret = array('status' => $status, 'count' => $start + sizeof($searchResults), 'mbox' => $mailbox, 'totalcount' => $total);
  1250. $GLOBALS['log']->info("INBOUNDEMAIL: $status : Downloaded " . $start + sizeof($searchResults) . "messages of $total");
  1251. } else {
  1252. $GLOBALS['log']->info("INBOUNDEMAIL: no results for mailbox [ {$mailbox} ]");
  1253. $ret = array('status' =>'done');
  1254. }
  1255. if ($ret['status'] == 'done') {
  1256. //Remove the cached search if we are done with this mailbox
  1257. $cacheFilePath = clean_path("{$this->EmailCachePath}/{$this->id}/folders/SearchData.php");
  1258. unlink($cacheFilePath);
  1259. /**
  1260. * To handle the use case where an external client is also connected, deleting emails, we need to clear our
  1261. * local cache of all emails with the "DELETED" flag
  1262. */
  1263. $criteria = 'DELETED';
  1264. $criteria .= (!empty($checkTime)) ? " SINCE \"{$checkTime}\"" : "";
  1265. $GLOBALS['log']->info("INBOUNDEMAIL: checking for deleted emails using [ {$criteria} ]");
  1266. $trashFolder = $this->get_stored_options("trashFolder");
  1267. if (empty($trashFolder)) {
  1268. $trashFolder = "INBOX.Trash";
  1269. }
  1270. if($this->mailbox != $trashFolder) {
  1271. $searchResults = imap_search($this->conn, $criteria, SE_UID);
  1272. if(!empty($searchResults)) {
  1273. $uids = implode($app_strings['LBL_EMAIL_DELIMITER'], $searchResults);
  1274. $GLOBALS['log']->info("INBOUNDEMAIL: removing UIDs found deleted [ {$uids} ]");
  1275. $this->getOverviewsFromCacheFile($uids, $mailbox, true);
  1276. }
  1277. }
  1278. }
  1279. return $ret;
  1280. }
  1281. function getCachedIMAPSearch($criteria) {
  1282. global $current_user;
  1283. global $sugar_config;
  1284. $cacheDataExists = false;
  1285. $diff = array();
  1286. $results = array();
  1287. $cacheFolderPath = clean_path("{$this->EmailCachePath}/{$this->id}/folders");
  1288. if (!file_exists($cacheFolderPath)) {
  1289. mkdir_recursive($cacheFolderPath);
  1290. }
  1291. $cacheFilePath = $cacheFolderPath . '/SearchData.php';
  1292. $GLOBALS['log']->info("INBOUNDEMAIL: Cache path is $cacheFilePath");
  1293. if(file_exists($cacheFilePath)) {
  1294. $cacheDataExists = true;
  1295. if($fh = @fopen($cacheFilePath, "rb")) {
  1296. $data = "";
  1297. $chunksize = 1*(1024*1024); // how many bytes per chunk
  1298. while(!feof($fh)) {
  1299. $buf = fgets($fh, $chunksize); // 8kb max buffer - shouldn't be more than 80 chars via pop3...
  1300. $data = $data . $buf;
  1301. flush();
  1302. } // while
  1303. fclose($fh);
  1304. $results = unserialize($data);
  1305. } // if
  1306. } // if
  1307. if (!$cacheDataExists) {
  1308. $searchResults = imap_search($this->conn, $criteria, SE_UID);
  1309. if(count($searchResults) > 0) {
  1310. $results = $searchResults;
  1311. $data = serialize($searchResults);
  1312. if($fh = @fopen($cacheFilePath, "w")) {
  1313. fputs($fh, $data);
  1314. fclose($fh);
  1315. } // if
  1316. }
  1317. } // if
  1318. return $results;
  1319. }
  1320. function checkEmailIMAPPartial($prefetch=true, $synch = false) {
  1321. $GLOBALS['log']->info("*****************INBOUNDEMAIL: at IMAP check partial");
  1322. global $sugar_config;
  1323. $result = $this->connectMailserver();
  1324. if ($result == 'false')
  1325. {
  1326. return array(
  1327. 'status' => 'error',
  1328. 'message' => 'Email server is down'
  1329. );
  1330. }
  1331. $mailboxes = $this->getMailboxes(true);
  1332. if (!in_array('INBOX', $mailboxes)) {
  1333. $mailboxes[] = 'INBOX';
  1334. }
  1335. sort($mailboxes);
  1336. if (isset($_REQUEST['mbox']) && !empty($_REQUEST['mbox']) && isset($_REQUEST['currentCount'])) {
  1337. $GLOBALS['log']->info("INBOUNDEMAIL: Picking up from where we left off");
  1338. $mbox = $_REQUEST['mbox'];
  1339. $count = $_REQUEST['currentCount'];
  1340. } else {
  1341. if ($synch) {
  1342. $GLOBALS['log']->info("INBOUNDEMAIL: Cleaning out the cache");
  1343. $this->cleanOutCache();
  1344. }
  1345. $mbox = $mailboxes[0];
  1346. $count = 0;
  1347. }
  1348. $GLOBALS['log']->info("INBOUNDEMAIL:found " . sizeof($mailboxes) . " Mailboxes");
  1349. $index = array_search($mbox, $mailboxes) + 1;
  1350. $ret = $this->checkEmailOneMailboxPartial($mbox, $prefetch, $synch, $count, 100);
  1351. while($ret['status'] == 'done' && $index < sizeof($mailboxes)) {
  1352. if ($ret['count'] > 100) {
  1353. $ret['mbox'] = $mailboxes[$index];
  1354. $ret['status'] = 'continue';
  1355. return $ret;
  1356. }
  1357. $GLOBALS['log']->info("INBOUNDEMAIL: checking account [ $index => $mbox : $count]");
  1358. $mbox = $mailboxes[$index];
  1359. $ret = $this->checkEmailOneMailboxPartial($mbox, $prefetch, $synch, 0, 100);
  1360. $index++;
  1361. }
  1362. return $ret;
  1363. }
  1364. function checkEmail2_meta() {
  1365. global $sugar_config;
  1366. $this->connectMailserver();
  1367. $mailboxes = $this->getMailboxes(true);
  1368. $mailboxes[] = 'INBOX';
  1369. sort($mailboxes);
  1370. $GLOBALS['log']->info("INBOUNDEMAIL: checking account [ {$this->name} ]");
  1371. $mailboxes_meta = array();
  1372. foreach($mailboxes as $mailbox) {
  1373. $mailboxes_meta[$mailbox] = $this->getMailboxProcessCount($mailbox);
  1374. }
  1375. $ret = array();
  1376. $ret['mailboxes'] = $mailboxes_meta;
  1377. foreach($mailboxes_meta as $count) {
  1378. $ret['processCount'] += $count;
  1379. }
  1380. return $ret;
  1381. }
  1382. function getMailboxProcessCount($mailbox) {
  1383. global $sugar_config;
  1384. $GLOBALS['log']->info("INBOUNDEMAIL: checking mailbox [ {$mailbox} ]");
  1385. $this->mailbox = $mailbox;
  1386. $this->connectMailserver();
  1387. $timestamp = $this->getCacheTimestamp($mailbox);
  1388. $checkTime = '';
  1389. if($timestamp > 0) {
  1390. $checkTime = date('r', $timestamp);
  1391. }
  1392. /* first time through, process ALL emails */
  1393. if(empty($checkTime)) {
  1394. $criteria = "ALL UNDELETED";
  1395. $prefetch = false; // do NOT prefetch emails on a brand new account - timeouts happen.
  1396. $GLOBALS['log']->info("INBOUNDEMAIL: new account detected - not prefetching email bodies.");
  1397. } else {
  1398. $criteria = "SINCE \"{$checkTime}\" UNDELETED"; // not using UNSEEN
  1399. }
  1400. $GLOBALS['log']->info("INBOUNDEMAIL: using [ {$criteria} ]");
  1401. $searchResults = imap_search($this->conn, $criteria, SE_UID);
  1402. if(!empty($searchResults)) {
  1403. $concatResults = implode(",", $searchResults);
  1404. } else {
  1405. $GLOBALS['log']->info("INBOUNDEMAIL: no results for mailbox [ {$mailbox} ]");
  1406. }
  1407. if(empty($searchResults)) {
  1408. return 0;
  1409. }
  1410. return count($searchResults);
  1411. }
  1412. /**
  1413. * update INBOX
  1414. */
  1415. function checkEmail($prefetch=true, $synch = false) {
  1416. global $sugar_config;
  1417. if($this->protocol == 'pop3') {
  1418. $this->pop3_checkEmail();
  1419. } else {
  1420. $this->connectMailserver();
  1421. $mailboxes = $this->getMailboxes(true);
  1422. sort($mailboxes);
  1423. $GLOBALS['log']->info("INBOUNDEMAIL: checking account [ {$this->name} ]");
  1424. foreach($mailboxes as $mailbox) {
  1425. $this->checkEmailOneMailbox($mailbox, $prefetch, $synch);
  1426. }
  1427. }
  1428. }
  1429. /**
  1430. * full synchronization
  1431. */
  1432. function syncEmail() {
  1433. global $sugar_config;
  1434. global $current_user;
  1435. $showFolders = unserialize(base64_decode($current_user->getPreference('showFolders', 'Emails')));
  1436. if(empty($showFolders)) {
  1437. $showFolders = array();
  1438. }
  1439. $email = new Email();
  1440. $email->email2init();
  1441. // personal accounts
  1442. if($current_user->hasPersonalEmail()) {
  1443. $personals = $this->retrieveByGroupId($current_user->id);
  1444. foreach($personals as $personalAccount) {
  1445. if(in_array($personalAccount->id, $showFolders)) {
  1446. $personalAccount->email = $email;
  1447. if ($personalAccount->isPop3Protocol()) {
  1448. $personalAccount->deletePop3Cache();
  1449. continue;
  1450. }
  1451. $personalAccount->cleanOutCache();
  1452. $personalAccount->connectMailserver();
  1453. $mailboxes = $personalAccount->getMailboxes(true);
  1454. $mailboxes[] = 'INBOX';
  1455. sort($mailboxes);
  1456. $GLOBALS['log']->info("[EMAIL] Start checking account [{$personalAccount->name}] for user [{$current_user->user_name}]");
  1457. foreach($mailboxes as $mailbox) {
  1458. $GLOBALS['log']->info("[EMAIL] Start checking mailbox [{$mailbox}] of account [{$personalAccount->name}] for user [{$current_user->user_name}]");
  1459. $personalAccount->checkEmailOneMailbox($mailbox, false, true);
  1460. $GLOBALS['log']->info("[EMAIL] Done checking mailbox [{$mailbox}] of account [{$personalAccount->name}] for user [{$current_user->user_name}]");
  1461. }
  1462. $GLOBALS['log']->info("[EMAIL] Done checking account [{$personalAccount->name}] for user [{$current_user->user_name}]");
  1463. }
  1464. }
  1465. }
  1466. // group accounts
  1467. $beans = $this->retrieveAllByGroupId($current_user->id, false);
  1468. foreach($beans as $k => $groupAccount) {
  1469. if(in_array($groupAccount->id, $showFolders)) {
  1470. $groupAccount->email = $email;
  1471. $groupAccount->cleanOutCache();
  1472. $groupAccount->connectMailserver();
  1473. $mailboxes = $groupAccount->getMailboxes(true);
  1474. $mailboxes[] = 'INBOX';
  1475. sort($mailboxes);
  1476. $GLOBALS['log']->info("INBOUNDEMAIL: checking account [ {$groupAccount->name} ]");
  1477. foreach($mailboxes as $mailbox) {
  1478. $groupAccount->checkEmailOneMailbox($mailbox, false, true);
  1479. }
  1480. }
  1481. }
  1482. }
  1483. /**
  1484. * Deletes cached messages when moving from folder to folder
  1485. * @param string $uids
  1486. * @param string $fromFolder
  1487. * @param string $toFolder
  1488. */
  1489. function deleteCachedMessages($uids, $fromFolder) {
  1490. global $sugar_config;
  1491. if(!isset($this->email) && !isset($this->email->et)) {
  1492. $this->email = new Email();
  1493. $this->email->email2init();
  1494. }
  1495. $uids = $this->email->et->_cleanUIDList($uids);
  1496. foreach($uids as $uid) {
  1497. $file = "{$this->EmailCachePath}/{$this->id}/messages/{$fromFolder}{$uid}.php";
  1498. if(file_exists($file)) {
  1499. if(!unlink($file)) {
  1500. $GLOBALS['log']->debug("INBOUNDEMAIL: Could not delete [ {$file} ]");
  1501. }
  1502. }
  1503. }
  1504. }
  1505. /**
  1506. * similar to imap_fetch_overview, but it gets overviews from a local cache
  1507. * file.
  1508. * @param string $uids UIDs in comma-delimited format
  1509. * @param string $mailbox The mailbox in focus, will default to $this->mailbox
  1510. * @param bool $remove Default false
  1511. * @return array
  1512. */
  1513. function getOverviewsFromCacheFile($uids, $mailbox='', $remove=false) {
  1514. global $app_strings;
  1515. if(!isset($this->email) && !isset($this->email->et)) {
  1516. $this->email = new Email();
  1517. $this->email->email2init();
  1518. }
  1519. $uids = $this->email->et->_cleanUIDList($uids, true);
  1520. // load current cache file
  1521. $mailbox = empty($mailbox) ? $this->mailbox : $mailbox;
  1522. $cacheValue = $this->getCacheValue($mailbox);
  1523. $ret = array();
  1524. // prep UID array
  1525. $exUids = explode($app_strings['LBL_EMAIL_DELIMITER'], $uids);
  1526. foreach($exUids as $k => $uid) {
  1527. $exUids[$k] = trim($uid);
  1528. }
  1529. // fill $ret will requested $uids
  1530. foreach($cacheValue['retArr'] as $k => $overview) {
  1531. if(in_array($overview->imap_uid, $exUids)) {
  1532. $ret[] = $overview;
  1533. }
  1534. }
  1535. // remove requested $uids from current cache file (move_mail() type action)
  1536. if($remove) {
  1537. $this->setCacheValue($mailbox, array(), array(), $ret);
  1538. }
  1539. return $ret;
  1540. }
  1541. /**
  1542. * merges new info with the saved cached file
  1543. * @param array $array Array of email Overviews
  1544. * @param string $type 'append' or 'remove'
  1545. * @param string $mailbox Target mailbox if not current assigned
  1546. */
  1547. function updateOverviewCacheFile($array, $type='append', $mailbox='') {
  1548. $mailbox = empty($mailbox) ? $this->mailbox : $mailbox;
  1549. $cacheValue = $this->getCacheValue($mailbox);
  1550. $uids = $cacheValue['uids'];
  1551. $updateRows = array();
  1552. $insertRows = array();
  1553. $removeRows = array();
  1554. // update values
  1555. if($type == 'append') { // append
  1556. /* we are adding overviews to the cache file */
  1557. foreach($array as $overview) {
  1558. if(isset($overview->uid)) {
  1559. $overview->imap_uid = $overview->uid; // coming from imap_fetch_overview() call
  1560. }
  1561. if(!in_array($overview->imap_uid, $uids)) {
  1562. $insertRows[] = $overview;
  1563. }
  1564. }
  1565. } else {
  1566. $updatedCacheOverviews = array();
  1567. // compare against generated list
  1568. /* we are removing overviews from the cache file */
  1569. foreach($cacheValue['retArr'] as $cacheOverview) {
  1570. if(!in_array($cacheOverview->imap_uid, $uids)) {
  1571. $insertRows[] = $cacheOverview;
  1572. } else {
  1573. $removeRows[] = $cacheOverview;
  1574. }
  1575. }
  1576. $cacheValue['retArr'] = $updatedCacheOverviews;
  1577. }
  1578. $this->setCacheValue($mailbox, $insertRows, $updateRows, $removeRows);
  1579. }
  1580. /**
  1581. * Check email prefetches email bodies for quicker display
  1582. * @param array array of fetched overviews
  1583. */
  1584. function fetchCheckedEmails($fetchedOverviews) {
  1585. global $sugar_config;
  1586. if(is_array($fetchedOverviews) && !empty($fetchedOverviews)) {
  1587. foreach($fetchedOverviews as $overview) {
  1588. if($overview->size < 10000) {
  1589. $uid = $overview->imap_uid;
  1590. if(!empty($uid)) {
  1591. $file = "{$this->mailbox}{$uid}.php";
  1592. $cacheFile = clean_path("{$this->EmailCachePath}/{$this->id}/messages/{$file}");
  1593. if(!file_exists($cacheFile)) {
  1594. $GLOBALS['log']->info("INBOUNDEMAIL: Prefetching email [ {$file} ]");
  1595. $this->setEmailForDisplay($uid);
  1596. $out = $this->displayOneEmail($uid, $this->mailbox);
  1597. $this->email->et->writeCacheFile('out', $out, $this->id, 'messages', "{$this->mailbox}{$uid}.php");
  1598. } else {
  1599. $GLOBALS['log']->debug("INBOUNDEMAIL: Trying to prefetch an email we already fetched! [ {$cacheFile} ]");
  1600. }
  1601. } else {
  1602. $GLOBALS['log']->debug("*** INBOUNDEMAIL: prefetch has a message with no UID");
  1603. }
  1604. return true;
  1605. } else {
  1606. $GLOBALS['log']->debug("INBOUNDEMAIL: skipping email prefetch - size too large [ {$overview->size} ]");
  1607. }
  1608. }
  1609. }
  1610. return false;
  1611. }
  1612. /**
  1613. * Sets flags on emails. Assumes that connection is live, correct folder is
  1614. * set.
  1615. * @param string $uids Sequence of UIDs, comma separated
  1616. * @param string $type Flag to mark
  1617. */
  1618. function markEmails($uids, $type) {
  1619. switch($type) {
  1620. case 'unread':
  1621. $result = imap_clearflag_full($this->conn, $uids, '\\SEEN', ST_UID);
  1622. break;
  1623. case 'read':
  1624. $result = imap_setflag_full($this->conn, $uids, '\\SEEN', ST_UID);
  1625. break;
  1626. case 'flagged':
  1627. $result = imap_setflag_full($this->conn, $uids, '\\FLAGGED', ST_UID);
  1628. break;
  1629. case 'unflagged':
  1630. $result = imap_clearflag_full($this->conn, $uids, '\\FLAGGED', ST_UID);
  1631. break;
  1632. case 'answered':
  1633. $result = imap_setflag_full($this->conn, $uids, '\\Answered', ST_UID);
  1634. break;
  1635. }
  1636. }
  1637. //// END EMAIL 2.0 SPECIFIC
  1638. ///////////////////////////////////////////////////////////////////////////
  1639. ///////////////////////////////////////////////////////////////////////////
  1640. //// SERVER MANIPULATION METHODS
  1641. /**
  1642. * Deletes the specified folder
  1643. * @param string $mbox "::" delimited IMAP mailbox path, ie, INBOX.saved.stuff
  1644. * @return bool
  1645. */
  1646. function deleteFolder($mbox) {
  1647. $returnArray = array();
  1648. if ($this->getCacheCount($mbox) > 0) {
  1649. $returnArray['status'] = false;
  1650. $returnArray['errorMessage'] = "Can not delete {$mbox} as it has emails.";
  1651. return $returnArray;
  1652. }
  1653. $connectString = $this->getConnectString('', $mbox);
  1654. //Remove Folder cache
  1655. global $sugar_config;
  1656. unlink("{$this->EmailCachePath}/{$this->id}/folders/folders.php");
  1657. if(imap_unsubscribe($this->conn, imap_utf7_encode($connectString))) {
  1658. if(imap_deletemailbox($this->conn, $connectString)) {
  1659. $this->mailbox = str_replace(("," . $mbox), "", $this->mailbox);
  1660. $this->save();
  1661. $sessionFoldersString = $this->getSessionInboundFoldersString($this->server_url, $this->email_user, $this->port, $this->protocol);
  1662. $sessionFoldersString = str_replace(("," . $mbox), "", $sessionFoldersString);
  1663. $this->setSessionInboundFoldersString($this->server_url, $this->email_user, $this->port, $this->protocol, $sessionFoldersString);
  1664. $returnArray['status'] = true;
  1665. return $returnArray;
  1666. } else {
  1667. $GLOBALS['log']->error("*** ERROR: EMAIL2.0 - could not delete IMAP mailbox with path: [ {$connectString} ]");
  1668. $returnArray['status'] = false;
  1669. $returnArray['errorMessage'] = "NOOP: could not delete folder: {$connectString}";
  1670. return $returnArray;
  1671. return false;
  1672. }
  1673. } else {
  1674. $GLOBALS['log']->error("*** ERROR: EMAIL2.0 - could not unsubscribe from folder, {$connectString} before deletion.");
  1675. $returnArray['status'] = false;
  1676. $returnArray['errorMessage'] = "NOOP: could not unsubscribe from folder, {$connectString} before deletion.";
  1677. return $returnArray;
  1678. }
  1679. }
  1680. /**
  1681. * Saves new folders
  1682. * @param string $name Name of new IMAP mailbox
  1683. * @param string $mbox "::" delimited IMAP mailbox path, ie, INBOX.saved.stuff
  1684. * @return bool True on success
  1685. */
  1686. function saveNewFolder($name, $mbox) {
  1687. global $sugar_config;
  1688. //Remove Folder cache
  1689. global $sugar_config;
  1690. //unlink("{$this->EmailCachePath}/{$this->id}/folders/folders.php");
  1691. //$mboxImap = $this->getImapMboxFromSugarProprietary($mbox);
  1692. $delimiter = $this->get_stored_options('folderDelimiter');
  1693. if (!$delimiter) {
  1694. $delimiter = '.';
  1695. }
  1696. $newFolder = $mbox . $delimiter . $name;
  1697. $mbox .= $delimiter.str_replace($delimiter, "_", $name);
  1698. $connectString = $this->getConnectString('', $mbox);
  1699. if(imap_createmailbox($this->conn, imap_utf7_encode($connectString))) {
  1700. imap_subscribe($this->conn, imap_utf7_encode($connectString));
  1701. $status = imap_status($this->conn, str_replace("{$delimiter}{$name}","",$connectString), SA_ALL);
  1702. $this->mailbox = $this->mailbox . "," . $newFolder;
  1703. $this->save();
  1704. $sessionFoldersString = $this->getSessionInboundFoldersString($this->server_url, $this->email_user, $this->port, $this->protocol);
  1705. $sessionFoldersString = $sessionFoldersString . "," . $newFolder;
  1706. $this->setSessionInboundFoldersString($this->server_url, $this->email_user, $this->port, $this->protocol, $sessionFoldersString);
  1707. echo json_encode($status);
  1708. return true;
  1709. } else {
  1710. echo "NOOP: could not create folder";
  1711. $GLOBALS['log']->error("*** ERROR: EMAIL2.0 - could not create IMAP mailbox with path: [ {$connectString} ]");
  1712. return false;
  1713. }
  1714. }
  1715. /**
  1716. * Constructs an IMAP c-client compatible folder path from Sugar proprietary
  1717. * @param string $mbox "::" delimited IMAP mailbox path, ie, INBOX.saved.stuff
  1718. * @return string
  1719. */
  1720. function getImapMboxFromSugarProprietary($mbox) {
  1721. $exMbox = explode("::", $mbox);
  1722. $mboxImap = '';
  1723. for($i=2; $i<count($exMbox); $i++) {
  1724. if(!empty($mboxImap)) {
  1725. $mboxImap .= ".";
  1726. }
  1727. $mboxImap .= $exMbox[$i];
  1728. }
  1729. return $mboxImap;
  1730. }
  1731. /**
  1732. * Searches IMAP (and POP3?) accounts/folders for emails with qualifying criteria
  1733. */
  1734. function search($ieId, $subject='', $from='', $to='', $body='', $dateFrom='', $dateTo='') {
  1735. global $current_user;
  1736. global $app_strings;
  1737. global $timedate;
  1738. $beans = array();
  1739. $bean = new InboundEmail();
  1740. $bean->retrieve($ieId);
  1741. $beans[] = $bean;
  1742. //$beans = $this->retrieveAllByGroupId($current_user->id, true);
  1743. $subject = urldecode($subject);
  1744. $criteria = "";
  1745. $criteria .= (!empty($subject)) ? 'SUBJECT '.from_html($subject).'' : "";
  1746. $criteria .= (!empty($from)) ? ' FROM "'.$from.'"' : "";
  1747. $criteria .= (!empty($to)) ? ' FROM "'.$to.'"' : "";
  1748. $criteria .= (!empty($body)) ? ' TEXT "'.$body.'"' : "";
  1749. $criteria .= (!empty($dateFrom)) ? ' SINCE "'.$timedate->fromString($dateFrom)->format('d-M-Y').'"' : "";
  1750. $criteria .= (!empty($dateTo)) ? ' BEFORE "'.$timedate->fromString($dateTo)->format('d-M-Y').'"' : "";
  1751. //$criteria .= (!empty($from)) ? ' FROM "'.$from.'"' : "";
  1752. $showFolders = unserialize(base64_decode($current_user->getPreference('showFolders', 'Emails')));
  1753. $out = array();
  1754. foreach($beans as $bean) {
  1755. if(!in_array($bean->id, $showFolders)) {
  1756. continue;
  1757. }
  1758. $GLOBALS['log']->info("*** INBOUNDEMAIL: searching [ {$bean->name} ] for [ {$subject}{$from}{$to}{$body}{$dateFrom}{$dateTo} ]");
  1759. $group = (!$bean->is_personal) ? 'group.' : '';
  1760. $bean->connectMailServer();
  1761. $mailboxes = $bean->getMailboxes(true);
  1762. if (!in_array('INBOX', $mailboxes)) {
  1763. $mailboxes[] = 'INBOX';
  1764. }
  1765. $totalHits = 0;
  1766. foreach($mailboxes as $mbox) {
  1767. $bean->mailbox = $mbox;
  1768. $searchOverviews = array();
  1769. if ($bean->protocol == 'pop3') {
  1770. $pop3Criteria = "SELECT * FROM email_cache WHERE ie_id = '{$bean->id}' AND mbox = '{$mbox}'";
  1771. $pop3Criteria .= (!empty($subject)) ? ' AND subject like "%'.$bean->db->quote($subject).'%"' : "";
  1772. $pop3Criteria .= (!empty($from)) ? ' AND fromaddr like "%'.$from.'%"' : "";
  1773. $pop3Criteria .= (!empty($to)) ? ' AND toaddr like "%'.$to.'%"' : "";
  1774. $pop3Criteria .= (!empty($dateFrom)) ? ' AND senddate > "'.$dateFrom.'"' : "";
  1775. $pop3Criteria .= (!empty($dateTo)) ? ' AND senddate < "'.$dateTo.'"' : "";
  1776. $GLOBALS['log']->info("*** INBOUNDEMAIL: searching [ {$mbox} ] using criteria [ {$pop3Criteria} ]");
  1777. $r = $bean->db->query($pop3Criteria);
  1778. while($a = $bean->db->fetchByAssoc($r)) {
  1779. $overview = new Overview();
  1780. foreach($a as $k => $v) {
  1781. $k=strtolower($k);
  1782. switch($k) {
  1783. case "imap_uid":
  1784. $overview->imap_uid = $v;
  1785. $overview->uid = $a['message_id'];
  1786. break;
  1787. case "toaddr":
  1788. $overview->to = from_html($v);
  1789. break;
  1790. case "fromaddr":
  1791. $overview->from = from_html($v);
  1792. break;
  1793. case "mailsize":
  1794. $overview->size = $v;
  1795. break;
  1796. case "senddate":
  1797. $overview->date = $timedate->fromString($v)->format('r');
  1798. break;
  1799. default:
  1800. $overview->$k = from_html($v);
  1801. break;
  1802. } // sqitch
  1803. } // foreach
  1804. $searchOverviews[] = $overview;
  1805. } // while
  1806. } else {
  1807. $bean->connectMailServer();
  1808. $searchResult = imap_search($bean->conn, $criteria, SE_UID);
  1809. if (!empty($searchResult)) {
  1810. $searchOverviews = imap_fetch_overview($bean->conn, implode(',', $searchResult), FT_UID);
  1811. } // if
  1812. } // else
  1813. $numHits = count($searchOverviews);
  1814. if($numHits > 0) {
  1815. $totalHits = $totalHits + $numHits;
  1816. $ret = $bean->sortFetchedOverview($searchOverviews, 'date', 'desc', true);
  1817. $mbox = "{$bean->id}.SEARCH";
  1818. $out = array_merge($out, $bean->displayFetchedSortedListXML($ret, $mbox, false));
  1819. }
  1820. }
  1821. }
  1822. $metadata = array();
  1823. $metadata['mbox'] = $app_strings['LBL_EMAIL_SEARCH_RESULTS_TITLE'];
  1824. $metadata['ieId'] = $this->id;
  1825. $metadata['name'] = $this->name;
  1826. $metadata['unreadChecked'] = ($current_user->getPreference('showUnreadOnly', 'Emails') == 1) ? 'CHECKED' : '';
  1827. $metadata['out'] = $out;
  1828. return $metadata;
  1829. }
  1830. /**
  1831. * repairs the encrypted password for a given I-E account
  1832. * @return bool True on success
  1833. */
  1834. function repairAccount() {
  1835. for($i=0; $i<3; $i++) {
  1836. if($i != 0) { // decode is performed on retrieve already
  1837. $this->email_password = blowfishDecode(blowfishGetKey('InboundEmail'), $this->email_password);
  1838. }
  1839. if($this->connectMailserver() == 'true') {
  1840. $this->save(); // save decoded password (is encoded on save())
  1841. return true;
  1842. }
  1843. }
  1844. return false;
  1845. }
  1846. /**
  1847. * soft deletes a User's personal inbox
  1848. * @param string id I-E id
  1849. * @param string user_name User name of User in focus, NOT current_user
  1850. * @return bool True on success
  1851. */
  1852. function deletePersonalEmailAccount($id, $user_name) {
  1853. $q = "SELECT ie.id FROM inbound_email ie LEFT JOIN users u ON ie.group_id = u.id WHERE u.user_name = '{$user_name}'";
  1854. $r = $this->db->query($q, true);
  1855. while($a = $this->db->fetchByAssoc($r)) {
  1856. if(!empty($a) && $a['id'] == $id) {
  1857. $this->retrieve($id);
  1858. $this->deleted = 1;
  1859. $this->save();
  1860. return true;
  1861. }
  1862. }
  1863. return false;
  1864. }
  1865. function getTeamSetIdForTeams($teamIds) {
  1866. if(!is_array($teamIds)){
  1867. $teamIds = array($teamIds);
  1868. } // if
  1869. $teamSet = new TeamSet();
  1870. $team_set_id = $teamSet->addTeams($teamIds);
  1871. return $team_set_id;
  1872. } // fn
  1873. /**
  1874. * Saves Personal Inbox settings for Users
  1875. * @param string userId ID of user to assign all emails for this account
  1876. * @param strings userName Name of account, for Sugar purposes
  1877. * @param bool forceSave Default true. Flag to save errored settings.
  1878. * @return boolean true on success, false on fail
  1879. */
  1880. function savePersonalEmailAccount($userId = '', $userName = '', $forceSave=true) {
  1881. $groupId = $userId;
  1882. $accountExists = false;
  1883. if(isset($_REQUEST['ie_id']) && !empty($_REQUEST['ie_id'])) {
  1884. $this->retrieve($_REQUEST['ie_id']);
  1885. $accountExists = true;
  1886. }
  1887. $ie_name = $_REQUEST['ie_name'];
  1888. $this->is_personal = 1;
  1889. $this->name = $ie_name;
  1890. $this->group_id = $groupId;
  1891. $this->status = $_REQUEST['ie_status'];
  1892. $this->server_url = trim($_REQUEST['server_url']);
  1893. $this->email_user = trim($_REQUEST['email_user']);
  1894. if(!empty($_REQUEST['email_password'])) {
  1895. $this->email_password = html_entity_decode($_REQUEST['email_password'], ENT_QUOTES);
  1896. }
  1897. $this->port = trim($_REQUEST['port']);
  1898. $this->protocol = $_REQUEST['protocol'];
  1899. if ($this->protocol == "pop3") {
  1900. $_REQUEST['mailbox'] = "INBOX";
  1901. }
  1902. $this->mailbox = $_REQUEST['mailbox'];
  1903. $this->mailbox_type = 'pick'; // forcing this
  1904. if(isset($_REQUEST['ssl']) && $_REQUEST['ssl'] == 1) { $useSsl = true; }
  1905. else $useSsl = false;
  1906. $this->service = '::::::::::';
  1907. if($forceSave) {
  1908. $id = $this->save(); // saving here to prevent user from having to re-enter all the info in case of error
  1909. $this->retrieve($id);
  1910. }
  1911. $this->protocol = $_REQUEST['protocol']; // need to set this again since we safe the "service" string to empty explode values
  1912. $opts = $this->getSessionConnectionString($this->server_url, $this->email_user, $this->port, $this->protocol);
  1913. $detectedOpts = $this->findOptimumSettings($useSsl);
  1914. //If $detectedOpts is empty, there was an error connecting, so clear $opts. If $opts was empty, use $detectedOpts
  1915. if (empty($opts) || empty($detectedOpts) || (empty($detectedOpts['good']) && empty($detectedOpts['serial'])))
  1916. {
  1917. $opts = $detectedOpts;
  1918. }
  1919. $delimiter = $this->getSessionInboundDelimiterString($this->server_url, $this->email_user, $this->port, $this->protocol);
  1920. if(isset($opts['serial']) && !empty($opts['serial'])) {
  1921. $this->service = $opts['serial'];
  1922. if(isset($_REQUEST['mark_read']) && $_REQUEST['mark_read'] == 1) {
  1923. $this->delete_seen = 0;
  1924. } else {
  1925. $this->delete_seen = 1;
  1926. }
  1927. // handle stored_options serialization
  1928. if(isset($_REQUEST['only_since']) && $_REQUEST['only_since'] == 1) {
  1929. $onlySince = true;
  1930. } else {
  1931. $onlySince = false;
  1932. }
  1933. $focusUser = new User();
  1934. $focusUser->retrieve($groupId);
  1935. $mailerId = (isset($_REQUEST['outbound_email'])) ? $_REQUEST['outbound_email'] : "";
  1936. $oe = new OutboundEmail();
  1937. $oe->getSystemMailerSettings($focusUser, $mailerId);
  1938. $stored_options = array();
  1939. $stored_options['from_name'] = trim($_REQUEST['from_name']);
  1940. $stored_options['from_addr'] = trim($_REQUEST['from_addr']);
  1941. $stored_options['reply_to_addr'] = trim($_REQUEST['reply_to_addr']);
  1942. if (!$this->isPop3Protocol()) {
  1943. $stored_options['trashFolder'] = (isset($_REQUEST['trashFolder']) ? trim($_REQUEST['trashFolder']) : "");
  1944. $stored_options['sentFolder'] = (isset($_REQUEST['sentFolder']) ? trim($_REQUEST['sentFolder']) : "");
  1945. } // if
  1946. $stored_options['only_since'] = $onlySince;
  1947. $stored_options['filter_domain'] = '';
  1948. $storedOptions['folderDelimiter'] = $delimiter;
  1949. $stored_options['outbound_email'] = (isset($_REQUEST['outbound_email'])) ? $_REQUEST['outbound_email'] : $oe->id;
  1950. $this->stored_options = base64_encode(serialize($stored_options));
  1951. $ieId = $this->save();
  1952. //If this is the first personal account the user has setup mark it as default for them.
  1953. $currentIECount = $this->getUserPersonalAccountCount($focusUser);
  1954. if($currentIECount == 1)
  1955. $this->setUsersDefaultOutboundServerId($focusUser, $ieId);
  1956. return true;
  1957. } else {
  1958. // could not find opts, no save
  1959. $GLOBALS['log']->debug('-----> InboundEmail could not find optimums for User: '.$ie_name);
  1960. return false;
  1961. }
  1962. }
  1963. /**
  1964. * Determines if this instance of I-E is for a Group Inbox or Personal Inbox
  1965. */
  1966. function handleIsPersonal() {
  1967. $qp = 'SELECT users.id, users.user_name FROM users WHERE users.is_group = 0 AND users.deleted = 0 AND users.status = \'active\' AND users.id = \''.$this->group_id.'\'';
  1968. $rp = $this->db->query($qp, true);
  1969. $personalBox = array();
  1970. while($ap = $this->db->fetchByAssoc($rp)) {
  1971. $personalBox[] = array($ap['id'], $ap['user_name']);
  1972. }
  1973. if(count($personalBox) > 0) {
  1974. return true;
  1975. } else {
  1976. return false;
  1977. }
  1978. }
  1979. function getUserNameFromGroupId() {
  1980. $r = $this->db->query('SELECT users.user_name FROM users WHERE deleted=0 AND id=\''.$this->group_id.'\'', true);
  1981. while($a = $this->db->fetchByAssoc($r)) {
  1982. return $a['user_name'];
  1983. }
  1984. return '';
  1985. }
  1986. function getFoldersListForMailBox() {
  1987. $return = array();
  1988. $foldersList = $this->getSessionInboundFoldersString($this->server_url, $this->email_user, $this->port, $this->protocol);
  1989. if (empty($foldersList)) {
  1990. global $mod_strings;
  1991. $msg = $this->connectMailserver(true);
  1992. if (strpos($msg, "successfully")) {
  1993. $foldersList = $this->getSessionInboundFoldersString($this->server_url, $this->email_user, $this->port, $this->protocol);
  1994. $return['status'] = true;
  1995. $return['foldersList'] = $foldersList;
  1996. $return['statusMessage'] = "";
  1997. } else {
  1998. $return['status'] = false;
  1999. $return['statusMessage'] = $msg;
  2000. } // else
  2001. } else {
  2002. $return['status'] = true;
  2003. $return['foldersList'] = $foldersList;
  2004. $return['statusMessage'] = "";
  2005. }
  2006. return $return;
  2007. } // fn
  2008. /**
  2009. * Programatically determines best-case settings for imap_open()
  2010. */
  2011. function findOptimumSettings($useSsl=false, $user='', $pass='', $server='', $port='', $prot='', $mailbox='') {
  2012. global $mod_strings;
  2013. $serviceArr = array();
  2014. $returnService = array();
  2015. $badService = array();
  2016. $goodService = array();
  2017. $errorArr = array();
  2018. $raw = array();
  2019. $retArray = array( 'good' => $goodService,
  2020. 'bad' => $badService,
  2021. 'err' => $errorArr);
  2022. if(!function_exists('imap_open')) {
  2023. $retArray['err'][0] = $mod_strings['ERR_NO_IMAP'];
  2024. return $retArray;
  2025. }
  2026. imap_errors(); // clearing error stack
  2027. error_reporting(0); // turn off notices from IMAP
  2028. if(isset($_REQUEST['ssl']) && $_REQUEST['ssl'] == 1) {
  2029. $useSsl = true;
  2030. }
  2031. $exServ = explode('::', $this->service);
  2032. $service = '/'.$exServ[1];
  2033. $nonSsl = array('both-secure' => '/notls/novalidate-cert/secure',
  2034. 'both' => '/notls/novalidate-cert',
  2035. 'nocert-secure' => '/novalidate-cert/secure',
  2036. 'nocert' => '/novalidate-cert',
  2037. 'notls-secure' => '/notls/secure',
  2038. 'secure' => '/secure', // for POP3 servers that force CRAM-MD5
  2039. 'notls' => '/notls',
  2040. 'none' => '', // try default nothing
  2041. );
  2042. $ssl = array(
  2043. 'ssl-both-on-secure' => '/ssl/tls/validate-cert/secure',
  2044. 'ssl-both-on' => '/ssl/tls/validate-cert',
  2045. 'ssl-cert-secure' => '/ssl/validate-cert/secure',
  2046. 'ssl-cert' => '/ssl/validate-cert',
  2047. 'ssl-tls-secure' => '/ssl/tls/secure',
  2048. 'ssl-tls' => '/ssl/tls',
  2049. 'ssl-both-off-secure' => '/ssl/notls/novalidate-cert/secure',
  2050. 'ssl-both-off' => '/ssl/notls/novalidate-cert',
  2051. 'ssl-nocert-secure' => '/ssl/novalidate-cert/secure',
  2052. 'ssl-nocert' => '/ssl/novalidate-cert',
  2053. 'ssl-notls-secure' => '/ssl/notls/secure',
  2054. 'ssl-notls' => '/ssl/notls',
  2055. 'ssl-secure' => '/ssl/secure',
  2056. 'ssl-none' => '/ssl',
  2057. );
  2058. if(isset($user) && !empty($user) && isset($pass) && !empty($pass)) {
  2059. $this->email_password = $pass;
  2060. $this->email_user = $user;
  2061. $this->server_url = $server;
  2062. $this->port = $port;
  2063. $this->protocol = $prot;
  2064. $this->mailbox = $mailbox;
  2065. }
  2066. // in case we flip from IMAP to POP3
  2067. if($this->protocol == 'pop3')
  2068. $this->mailbox = 'INBOX';
  2069. //If user has selected multiple mailboxes, we only need to test the first mailbox for the connection string.
  2070. $a_mailbox = explode(",", $this->mailbox);
  2071. $tmpMailbox = isset($a_mailbox[0]) ? $a_mailbox[0] : "";
  2072. if($useSsl == true)
  2073. {
  2074. foreach($ssl as $k => $service)
  2075. {
  2076. $returnService[$k] = 'foo'.$service;
  2077. $serviceArr[$k] = '{'.$this->server_url.':'.$this->port.'/service='.$this->protocol.$service.'}'.$tmpMailbox;
  2078. }
  2079. }
  2080. else
  2081. {
  2082. foreach($nonSsl as $k => $service)
  2083. {
  2084. $returnService[$k] = 'foo'.$service;
  2085. $serviceArr[$k] = '{'.$this->server_url.':'.$this->port.'/service='.$this->protocol.$service.'}'.$tmpMailbox;
  2086. }
  2087. }
  2088. $GLOBALS['log']->debug('---------------STARTING FINDOPTIMUMS LOOP----------------');
  2089. $l = 1;
  2090. //php imap library will capture c-client library warnings as errors causing good connections to be ignored.
  2091. //Check against known warnings to ensure good connections are used.
  2092. $acceptableWarnings = array("SECURITY PROBLEM: insecure server advertised AUTH=PLAIN", //c-client auth_pla.c
  2093. "Mailbox is empty");
  2094. $login = $this->email_user;
  2095. $passw = $this->email_password;
  2096. $foundGoodConnection = false;
  2097. foreach($serviceArr as $k => $serviceTest) {
  2098. $errors = '';
  2099. $alerts = '';
  2100. $GLOBALS['log']->debug($l.': I-E testing string: '.$serviceTest);
  2101. // open the connection and try the test string
  2102. $this->conn = imap_open($serviceTest, $login, $passw);
  2103. if(($errors = imap_last_error()) || ($alerts = imap_alerts())) {
  2104. if($errors == 'Too many login failures' || $errors == '[CLOSED] IMAP connection broken (server response)') { // login failure means don't bother trying the rest
  2105. $GLOBALS['log']->debug($l.': I-E failed using ['.$serviceTest.']');
  2106. $retArray['err'][$k] = $mod_strings['ERR_BAD_LOGIN_PASSWORD'];
  2107. $retArray['bad'][$k] = $serviceTest;
  2108. $GLOBALS['log']->debug($l.': I-E ERROR: $ie->findOptimums() failed due to bad user credentials for user login: '.$this->email_user);
  2109. return $retArray;
  2110. } elseif( in_array($errors, $acceptableWarnings, TRUE)) { // false positive
  2111. $GLOBALS['log']->debug($l.': I-E found good connection but with warnings ['.$serviceTest.'] Errors:' . $errors);
  2112. $retArray['good'][$k] = $returnService[$k];
  2113. $foundGoodConnection = true;
  2114. }
  2115. else {
  2116. $GLOBALS['log']->debug($l.': I-E failed using ['.$serviceTest.'] - error: '.$errors);
  2117. $retArray['err'][$k] = $errors;
  2118. $retArray['bad'][$k] = $serviceTest;
  2119. }
  2120. } else {
  2121. $GLOBALS['log']->debug($l.': I-E found good connect using ['.$serviceTest.']');
  2122. $retArray['good'][$k] = $returnService[$k];
  2123. $foundGoodConnection = true;
  2124. }
  2125. if(is_resource($this->conn)) {
  2126. if (!$this->isPop3Protocol()) {
  2127. $serviceTest = str_replace("INBOX", "", $serviceTest);
  2128. $boxes = imap_getmailboxes($this->conn, $serviceTest, "*");
  2129. $delimiter = '.';
  2130. // clean MBOX path names
  2131. foreach($boxes as $k => $mbox) {
  2132. $raw[] = $mbox->name;
  2133. if ($mbox->delimiter) {
  2134. $delimiter = $mbox->delimiter;
  2135. } // if
  2136. } // foreach
  2137. $this->setSessionInboundDelimiterString($this->server_url, $this->email_user, $this->port, $this->protocol, $delimiter);
  2138. } // if
  2139. if(!imap_close($this->conn)) $GLOBALS['log']->debug('imap_close() failed!');
  2140. }
  2141. $GLOBALS['log']->debug($l.': I-E clearing error and alert stacks.');
  2142. imap_errors(); // clear stacks
  2143. imap_alerts();
  2144. // If you find a good connection, then don't do any further testing to find URL
  2145. if ($foundGoodConnection) {
  2146. break;
  2147. } // if
  2148. $l++;
  2149. }
  2150. $GLOBALS['log']->debug('---------------end FINDOPTIMUMS LOOP----------------');
  2151. if(!empty($retArray['good'])) {
  2152. $newTls = '';
  2153. $newCert = '';
  2154. $newSsl = '';
  2155. $newNotls = '';
  2156. $newNovalidate_cert = '';
  2157. $good = array_pop($retArray['good']); // get most complete string
  2158. $exGood = explode('/', $good);
  2159. foreach($exGood as $v) {
  2160. switch($v) {
  2161. case 'ssl':
  2162. $newSsl = 'ssl';
  2163. break;
  2164. case 'tls':
  2165. $newTls = 'tls';
  2166. break;
  2167. case 'notls':
  2168. $newNotls = 'notls';
  2169. break;
  2170. case 'cert':
  2171. $newCert = 'validate-cert';
  2172. break;
  2173. case 'novalidate-cert':
  2174. $newNovalidate_cert = 'novalidate-cert';
  2175. break;
  2176. case 'secure':
  2177. $secure = 'secure';
  2178. break;
  2179. }
  2180. }
  2181. $goodStr['serial'] = $newTls.'::'.$newCert.'::'.$newSsl.'::'.$this->protocol.'::'.$newNovalidate_cert.'::'.$newNotls.'::'.$secure;
  2182. $goodStr['service'] = $good;
  2183. $testConnectString = str_replace('foo','', $good);
  2184. $testConnectString = '{'.$this->server_url.':'.$this->port.'/service='.$this->protocol.$testConnectString.'}';
  2185. $this->setSessionConnectionString($this->server_url, $this->email_user, $this->port, $this->protocol, $goodStr);
  2186. $i = 0;
  2187. foreach($raw as $mbox)
  2188. {
  2189. $raw[$i] = str_replace($testConnectString, "", $GLOBALS['locale']->translateCharset($mbox, "UTF7-IMAP", "UTF8" ));
  2190. $i++;
  2191. } // foreach
  2192. sort($raw);
  2193. $this->setSessionInboundFoldersString($this->server_url, $this->email_user, $this->port, $this->protocol, implode(",", $raw));
  2194. return $goodStr;
  2195. } else {
  2196. return false;
  2197. }
  2198. }
  2199. function getSessionConnectionString($server_url, $email_user, $port, $protocol) {
  2200. $sessionConnectionString = $server_url . $email_user . $port . $protocol;
  2201. return (isset($_SESSION[$sessionConnectionString]) ? $_SESSION[$sessionConnectionString] : "");
  2202. }
  2203. function setSessionConnectionString($server_url, $email_user, $port, $protocol, $goodStr) {
  2204. $sessionConnectionString = $server_url . $email_user . $port . $protocol;
  2205. $_SESSION[$sessionConnectionString] = $goodStr;
  2206. }
  2207. function getSessionInboundDelimiterString($server_url, $email_user, $port, $protocol) {
  2208. $sessionInboundDelimiterString = $server_url . $email_user . $port . $protocol . "delimiter";
  2209. return (isset($_SESSION[$sessionInboundDelimiterString]) ? $_SESSION[$sessionInboundDelimiterString] : "");
  2210. }
  2211. function setSessionInboundDelimiterString($server_url, $email_user, $port, $protocol, $delimiter) {
  2212. $sessionInboundDelimiterString = $server_url . $email_user . $port . $protocol . "delimiter";
  2213. $_SESSION[$sessionInboundDelimiterString] = $delimiter;
  2214. }
  2215. function getSessionInboundFoldersString($server_url, $email_user, $port, $protocol) {
  2216. $sessionInboundFoldersListString = $server_url . $email_user . $port . $protocol . "foldersList";
  2217. return (isset($_SESSION[$sessionInboundFoldersListString]) ? $_SESSION[$sessionInboundFoldersListString] : "");
  2218. }
  2219. function setSessionInboundFoldersString($server_url, $email_user, $port, $protocol, $foldersList) {
  2220. $sessionInboundFoldersListString = $server_url . $email_user . $port . $protocol . "foldersList";
  2221. $_SESSION[$sessionInboundFoldersListString] = $foldersList;
  2222. }
  2223. /**
  2224. * Checks for duplicate Group User names when creating a new one at save()
  2225. * @return GUID returns GUID of Group User if user_name match is
  2226. * found
  2227. * @return boolean false if NO DUPE IS FOUND
  2228. */
  2229. function groupUserDupeCheck() {
  2230. $q = "SELECT u.id FROM users u WHERE u.deleted=0 AND u.is_group=1 AND u.user_name = '".$this->name."'";
  2231. $r = $this->db->query($q, true);
  2232. $uid = '';
  2233. while($a = $this->db->fetchByAssoc($r)) {
  2234. $uid = $a['id'];
  2235. }
  2236. if(strlen($uid) > 0) {
  2237. return $uid;
  2238. } else {
  2239. return false;
  2240. }
  2241. }
  2242. /**
  2243. * Returns <option> markup with the contents of Group users
  2244. * @param array $groups default empty array
  2245. * @return string HTML options
  2246. */
  2247. function getGroupsWithSelectOptions($groups = array()) {
  2248. $r = $this->db->query('SELECT id, user_name FROM users WHERE users.is_group = 1 AND deleted = 0', true);
  2249. if(is_resource($r)) {
  2250. while($a = $this->db->fetchByAssoc($r)) {
  2251. $groups[$a['id']] = $a['user_name'];
  2252. }
  2253. }
  2254. $selectOptions = get_select_options_with_id_separate_key($groups, $groups, $this->group_id);
  2255. return $selectOptions;
  2256. }
  2257. /**
  2258. * handles auto-responses to inbound emails
  2259. *
  2260. * @param object email Email passed as reference
  2261. */
  2262. function handleAutoresponse(&$email, &$contactAddr) {
  2263. if($this->template_id) {
  2264. $GLOBALS['log']->debug('found auto-reply template id - prefilling and mailing response');
  2265. if($this->getAutoreplyStatus($contactAddr)
  2266. && $this->checkOutOfOffice($email->name)
  2267. && $this->checkFilterDomain($email)) { // if we haven't sent this guy 10 replies in 24hours
  2268. if(!empty($this->stored_options)) {
  2269. $storedOptions = unserialize(base64_decode($this->stored_options));
  2270. }
  2271. // get FROM NAME
  2272. if(!empty($storedOptions['from_name'])) {
  2273. $from_name = $storedOptions['from_name'];
  2274. $GLOBALS['log']->debug('got from_name from storedOptions: '.$from_name);
  2275. } else { // use system default
  2276. $rName = $this->db->query('SELECT value FROM config WHERE name = \'fromname\'', true);
  2277. if(is_resource($rName)) {
  2278. $aName = $this->db->fetchByAssoc($rName);
  2279. }
  2280. if(!empty($aName['value'])) {
  2281. $from_name = $aName['value'];
  2282. } else {
  2283. $from_name = '';
  2284. }
  2285. }
  2286. // get FROM ADDRESS
  2287. if(!empty($storedOptions['from_addr'])) {
  2288. $from_addr = $storedOptions['from_addr'];
  2289. } else {
  2290. $rAddr = $this->db->query('SELECT value FROM config WHERE name = \'fromaddress\'', true);
  2291. if(is_resource($rAddr)) {
  2292. $aAddr = $this->db->fetchByAssoc($rAddr);
  2293. }
  2294. if(!empty($aAddr['value'])) {
  2295. $from_addr = $aAddr['value'];
  2296. } else {
  2297. $from_addr = '';
  2298. }
  2299. }
  2300. $replyToName = (!empty($storedOptions['reply_to_name']))? from_html($storedOptions['reply_to_name']) :$from_name ;
  2301. $replyToAddr = (!empty($storedOptions['reply_to_addr'])) ? $storedOptions['reply_to_addr'] : $from_addr;
  2302. if(!empty($email->reply_to_email)) {
  2303. $to[0]['email'] = $email->reply_to_email;
  2304. } else {
  2305. $to[0]['email'] = $email->from_addr;
  2306. }
  2307. // handle to name: address, prefer reply-to
  2308. if(!empty($email->reply_to_name)) {
  2309. $to[0]['display'] = $email->reply_to_name;
  2310. } elseif(!empty($email->from_name)) {
  2311. $to[0]['display'] = $email->from_name;
  2312. }
  2313. $et = new EmailTemplate();
  2314. $et->retrieve($this->template_id);
  2315. if(empty($et->subject)) { $et->subject = ''; }
  2316. if(empty($et->body)) { $et->body = ''; }
  2317. if(empty($et->body_html)) { $et->body_html = ''; }
  2318. $reply = new Email();
  2319. $reply->type = 'out';
  2320. $reply->to_addrs = $to[0]['email'];
  2321. $reply->to_addrs_arr = $to;
  2322. $reply->cc_addrs_arr = array();
  2323. $reply->bcc_addrs_arr = array();
  2324. $reply->from_name = $from_name;
  2325. $reply->from_addr = $from_addr;
  2326. $reply->name = $et->subject;
  2327. $reply->description = $et->body;
  2328. $reply->description_html = $et->body_html;
  2329. $reply->reply_to_name = $replyToName;
  2330. $reply->reply_to_addr = $replyToAddr;
  2331. $GLOBALS['log']->debug('saving and sending auto-reply email');
  2332. //$reply->save(); // don't save the actual email.
  2333. $reply->send();
  2334. $this->setAutoreplyStatus($contactAddr);
  2335. } else {
  2336. $GLOBALS['log']->debug('InboundEmail: auto-reply threshold reached for email ('.$contactAddr.') - not sending auto-reply');
  2337. }
  2338. }
  2339. }
  2340. function handleCaseAssignment($email) {
  2341. $c = new aCase();
  2342. if($caseId = $this->getCaseIdFromCaseNumber($email->name, $c)) {
  2343. $c->retrieve($caseId);
  2344. $email->retrieve($email->id);
  2345. //assign the case info to parent id and parent type so that the case can be linked to the email on Email Save
  2346. $email->parent_type = "Cases";
  2347. $email->parent_id = $caseId;
  2348. // assign the email to the case owner
  2349. $email->assigned_user_id = $c->assigned_user_id;
  2350. $email->save();
  2351. $GLOBALS['log']->debug('InboundEmail found exactly 1 match for a case: '.$c->name);
  2352. return true;
  2353. } // if
  2354. return false;
  2355. } // fn
  2356. /**
  2357. * handles functionality specific to the Mailbox type (Cases, bounced
  2358. * campaigns, etc.)
  2359. *
  2360. * @param object email Email object passed as a reference
  2361. * @param object header Header object generated by imap_headerinfo();
  2362. */
  2363. function handleMailboxType(&$email, &$header) {
  2364. switch($this->mailbox_type) {
  2365. case 'support':
  2366. $this->handleCaseAssignment($email);
  2367. break;
  2368. case 'bug':
  2369. break;
  2370. case 'info':
  2371. // do something with this?
  2372. break;
  2373. case 'sales':
  2374. // do something with leads? we don't have an email_leads table
  2375. break;
  2376. case 'task':
  2377. // do something?
  2378. break;
  2379. case 'bounce':
  2380. require_once('modules/Campaigns/ProcessBouncedEmails.php');
  2381. campaign_process_bounced_emails($email, $header);
  2382. break;
  2383. case 'pick': // do all except bounce handling
  2384. $GLOBALS['log']->debug('looking for a case for '.$email->name);
  2385. $this->handleCaseAssignment($email);
  2386. break;
  2387. }
  2388. }
  2389. function isMailBoxTypeCreateCase() {
  2390. return ($this->mailbox_type == 'createcase' && !empty($this->groupfolder_id));
  2391. } // fn
  2392. function handleCreateCase($email, $userId) {
  2393. global $current_user, $mod_strings, $current_language;
  2394. $mod_strings = return_module_language($current_language, "Emails");
  2395. $GLOBALS['log']->debug('In handleCreateCase');
  2396. $c = new aCase();
  2397. $this->getCaseIdFromCaseNumber($email->name, $c);
  2398. if (!$this->handleCaseAssignment($email) && $this->isMailBoxTypeCreateCase()) {
  2399. // create a case
  2400. $GLOBALS['log']->debug('retrieveing email');
  2401. $email->retrieve($email->id);
  2402. $c = new aCase();
  2403. $c->description = $email->description;
  2404. $c->assigned_user_id = $userId;
  2405. $c->name = $email->name;
  2406. $c->status = 'New';
  2407. $c->priority = 'P1';
  2408. if(!empty($email->reply_to_email)) {
  2409. $contactAddr = $email->reply_to_email;
  2410. } else {
  2411. $contactAddr = $email->from_addr;
  2412. }
  2413. $GLOBALS['log']->debug('finding related accounts with address ' . $contactAddr);
  2414. if($accountIds = $this->getRelatedId($contactAddr, 'accounts')) {
  2415. if (sizeof($accountIds) == 1) {
  2416. $c->account_id = $accountIds[0];
  2417. $acct = new Account();
  2418. $acct->retrieve($c->account_id);
  2419. $c->account_name = $acct->name;
  2420. } // if
  2421. } // if
  2422. $c->save(true);
  2423. $caseId = $c->id;
  2424. $c = new aCase();
  2425. $c->retrieve($caseId);
  2426. if($c->load_relationship('emails')) {
  2427. $c->emails->add($email->id);
  2428. } // if
  2429. if($contactIds = $this->getRelatedId($contactAddr, 'contacts')) {
  2430. if(!empty($contactIds) && $c->load_relationship('contacts')) {
  2431. $c->contacts->add($contactIds);
  2432. } // if
  2433. } // if
  2434. $c->email_id = $email->id;
  2435. $email->parent_type = "Cases";
  2436. $email->parent_id = $caseId;
  2437. // assign the email to the case owner
  2438. $email->assigned_user_id = $c->assigned_user_id;
  2439. $email->name = str_replace('%1', $c->case_number, $c->getEmailSubjectMacro()) . " ". $email->name;
  2440. $email->save();
  2441. $GLOBALS['log']->debug('InboundEmail created one case with number: '.$c->case_number);
  2442. $createCaseTemplateId = $this->get_stored_options('create_case_email_template', "");
  2443. if(!empty($this->stored_options)) {
  2444. $storedOptions = unserialize(base64_decode($this->stored_options));
  2445. }
  2446. if(!empty($createCaseTemplateId)) {
  2447. $fromName = "";
  2448. $fromAddress = "";
  2449. if (!empty($this->stored_options)) {
  2450. $fromAddress = $storedOptions['from_addr'];
  2451. $fromName = from_html($storedOptions['from_name']);
  2452. $replyToName = (!empty($storedOptions['reply_to_name']))? from_html($storedOptions['reply_to_name']) :$fromName ;
  2453. $replyToAddr = (!empty($storedOptions['reply_to_addr'])) ? $storedOptions['reply_to_addr'] : $fromAddress;
  2454. } // if
  2455. $defaults = $current_user->getPreferredEmail();
  2456. $fromAddress = (!empty($fromAddress)) ? $fromAddress : $defaults['email'];
  2457. $fromName = (!empty($fromName)) ? $fromName : $defaults['name'];
  2458. $to[0]['email'] = $contactAddr;
  2459. // handle to name: address, prefer reply-to
  2460. if(!empty($email->reply_to_name)) {
  2461. $to[0]['display'] = $email->reply_to_name;
  2462. } elseif(!empty($email->from_name)) {
  2463. $to[0]['display'] = $email->from_name;
  2464. }
  2465. $et = new EmailTemplate();
  2466. $et->retrieve($createCaseTemplateId);
  2467. if(empty($et->subject)) { $et->subject = ''; }
  2468. if(empty($et->body)) { $et->body = ''; }
  2469. if(empty($et->body_html)) { $et->body_html = ''; }
  2470. $et->subject = "Re:" . " " . str_replace('%1', $c->case_number, $c->getEmailSubjectMacro() . " ". $c->name);
  2471. $html = trim($email->description_html);
  2472. $plain = trim($email->description);
  2473. $email->email2init();
  2474. $email->from_addr = $email->from_addr_name;
  2475. $email->to_addrs = $email->to_addrs_names;
  2476. $email->cc_addrs = $email->cc_addrs_names;
  2477. $email->bcc_addrs = $email->bcc_addrs_names;
  2478. $email->from_name = $email->from_addr;
  2479. $email = $email->et->handleReplyType($email, "reply");
  2480. $ret = $email->et->displayComposeEmail($email);
  2481. $ret['description'] = empty($email->description_html) ? str_replace("\n", "\n<BR/>", $email->description) : $email->description_html;
  2482. $reply = new Email();
  2483. $reply->type = 'out';
  2484. $reply->to_addrs = $to[0]['email'];
  2485. $reply->to_addrs_arr = $to;
  2486. $reply->cc_addrs_arr = array();
  2487. $reply->bcc_addrs_arr = array();
  2488. $reply->from_name = $fromName;
  2489. $reply->from_addr = $fromAddress;
  2490. $reply->reply_to_name = $replyToName;
  2491. $reply->reply_to_addr = $replyToAddr;
  2492. $reply->name = $et->subject;
  2493. $reply->description = $et->body . "<div><hr /></div>" . $email->description;
  2494. if (!$et->text_only) {
  2495. $reply->description_html = $et->body_html . "<div><hr /></div>" . $email->description;
  2496. }
  2497. $GLOBALS['log']->debug('saving and sending auto-reply email');
  2498. //$reply->save(); // don't save the actual email.
  2499. $reply->send();
  2500. } // if
  2501. } else {
  2502. if(!empty($email->reply_to_email)) {
  2503. $contactAddr = $email->reply_to_email;
  2504. } else {
  2505. $contactAddr = $email->from_addr;
  2506. }
  2507. $this->handleAutoresponse($email, $contactAddr);
  2508. }
  2509. } // fn
  2510. /**
  2511. * handles linking contacts, accounts, etc. to an email
  2512. *
  2513. * @param object Email bean to be linked against
  2514. * @return string contactAddr is the email address of the sender
  2515. */
  2516. function handleLinking(&$email) {
  2517. // link email to an User if emails match TO addr
  2518. if($userIds = $this->getRelatedId($email->to_addrs, 'users')) {
  2519. $GLOBALS['log']->debug('I-E linking email to User');
  2520. // link the user to the email
  2521. $email->load_relationship('users');
  2522. $email->users->add($userIds);
  2523. }
  2524. // link email to a Contact, Lead, or Account if the emails match
  2525. // give precedence to REPLY-TO above FROM
  2526. if(!empty($email->reply_to_email)) {
  2527. $contactAddr = $email->reply_to_email;
  2528. } else {
  2529. $contactAddr = $email->from_addr;
  2530. }
  2531. // Samir Gandhi : 12/06/07
  2532. // This changes has been done because the linking was done only with the from address and
  2533. // not with to address
  2534. $relationShipAddress = $contactAddr;
  2535. if (empty($relationShipAddress)) {
  2536. $relationShipAddress .= $email->to_addrs;
  2537. } else {
  2538. $relationShipAddress = $relationShipAddress . "," . $email->to_addrs;
  2539. }
  2540. if($leadIds = $this->getRelatedId($relationShipAddress, 'leads')) {
  2541. $GLOBALS['log']->debug('I-E linking email to Lead');
  2542. $email->load_relationship('leads');
  2543. $email->leads->add($leadIds);
  2544. foreach($leadIds as $leadId) {
  2545. $lead = new Lead();
  2546. $lead->retrieve($leadId);
  2547. $lead->load_relationship('emails');
  2548. $lead->emails->add($email->id);
  2549. }
  2550. }
  2551. if($contactIds = $this->getRelatedId($relationShipAddress, 'contacts')) {
  2552. $GLOBALS['log']->debug('I-E linking email to Contact');
  2553. // link the contact to the email
  2554. $email->load_relationship('contacts');
  2555. $email->contacts->add($contactIds);
  2556. }
  2557. if($accountIds = $this->getRelatedId($relationShipAddress, 'accounts')) {
  2558. $GLOBALS['log']->debug('I-E linking email to Account');
  2559. // link the account to the email
  2560. $email->load_relationship('accounts');
  2561. $email->accounts->add($accountIds);
  2562. /* cn: bug 9171 another cause of dying I-E - bad linking
  2563. foreach($accountIds as $accountId) {
  2564. $GLOBALS['log']->debug('I-E reverse-linking Accounts to Emails');
  2565. $acct = new Account();
  2566. $acct->retrieve($accountId);
  2567. $acct->load_relationship('emails');
  2568. $acct->account_emails->add($email->id);
  2569. }
  2570. */
  2571. }
  2572. return $contactAddr;
  2573. }
  2574. /**
  2575. * Gets part by following breadcrumb path
  2576. * @param string $bc the breadcrumb string in format (1.1.1)
  2577. * @param array parts the root level parts array
  2578. */
  2579. protected function getPartByPath($bc, $parts)
  2580. {
  2581. if(strstr($bc,'.')) {
  2582. $exBc = explode('.', $bc);
  2583. } else {
  2584. $exBc = array($bc);
  2585. }
  2586. foreach($exBc as $step) {
  2587. if(empty($parts)) return false;
  2588. $res = $parts[$step-1]; // MIME starts with 1, array starts with 0
  2589. if(!empty($res->parts)) {
  2590. $parts = $res->parts;
  2591. } else {
  2592. $parts = false;
  2593. }
  2594. }
  2595. return $res;
  2596. }
  2597. /**
  2598. * takes a breadcrumb and returns the encoding at that level
  2599. * @param string bc the breadcrumb string in format (1.1.1)
  2600. * @param array parts the root level parts array
  2601. * @return int retInt Int key to transfer encoding (see handleTranserEncoding())
  2602. */
  2603. function getEncodingFromBreadCrumb($bc, $parts) {
  2604. if(strstr($bc,'.')) {
  2605. $exBc = explode('.', $bc);
  2606. } else {
  2607. $exBc[0] = $bc;
  2608. }
  2609. $depth = count($exBc);
  2610. for($i=0; $i<$depth; $i++) {
  2611. $tempObj[$i] = $parts[($exBc[$i]-1)];
  2612. $retInt = imap_utf8($tempObj[$i]->encoding);
  2613. if(!empty($tempObj[$i]->parts)) {
  2614. $parts = $tempObj[$i]->parts;
  2615. }
  2616. }
  2617. return $retInt;
  2618. }
  2619. /**
  2620. * retrieves the charset for a given part of an email body
  2621. *
  2622. * @param string bc target part of the message in format (1.1.1)
  2623. * @param array parts 1 level above ROOT array of Objects representing a multipart body
  2624. * @return string charset name
  2625. */
  2626. function getCharsetFromBreadCrumb($bc, $parts)
  2627. {
  2628. $tempObj = $this->getPartByPath($bc, $parts);
  2629. // now we have the tempObj at the end of the breadCrumb trail
  2630. if(!empty($tempObj->ifparameters)) {
  2631. foreach($tempObj->parameters as $param) {
  2632. if(strtolower($param->attribute) == 'charset') {
  2633. return $param->value;
  2634. }
  2635. }
  2636. }
  2637. return 'default';
  2638. }
  2639. /**
  2640. * Get the message text from a single mime section, html or plain.
  2641. *
  2642. * @param string $msgNo
  2643. * @param string $section
  2644. * @param stdObject $structure
  2645. * @return string
  2646. */
  2647. function getMessageTextFromSingleMimePart($msgNo,$section,$structure)
  2648. {
  2649. $msgPartTmp = imap_fetchbody($this->conn, $msgNo, $section);
  2650. $enc = $this->getEncodingFromBreadCrumb($section, $structure->parts);
  2651. $charset = $this->getCharsetFromBreadCrumb($section, $structure->parts);
  2652. $msgPartTmp = $this->handleTranserEncoding($msgPartTmp, $enc);
  2653. return $this->handleCharsetTranslation($msgPartTmp, $charset);
  2654. }
  2655. /**
  2656. * Givin an existing breadcrumb add a cooresponding offset
  2657. *
  2658. * @param string $bc
  2659. * @param string $offset
  2660. * @return string
  2661. */
  2662. function addBreadCrumbOffset($bc, $offset)
  2663. {
  2664. if( (empty($bc) || is_null($bc)) && !empty($offset) )
  2665. return $offset;
  2666. $a_bc = explode(".", $bc);
  2667. $a_offset = explode(".",$offset);
  2668. if(count($a_bc) < count($a_offset))
  2669. $a_bc = array_merge($a_bc,array_fill( count($a_bc), count($a_offset) - count($a_bc), 0));
  2670. $results = array();
  2671. for($i=0;$i < count($a_bc); $i++)
  2672. {
  2673. if(isset($a_offset[$i]))
  2674. $results[] = $a_bc[$i] + $a_offset[$i];
  2675. else
  2676. $results[] = $a_bc[$i];
  2677. }
  2678. return implode(".", $results);
  2679. }
  2680. /**
  2681. * returns the HTML text part of a multi-part message
  2682. *
  2683. * @param int msgNo the relative message number for the monitored mailbox
  2684. * @param string $type the type of text processed, either 'PLAIN' or 'HTML'
  2685. * @return string UTF-8 encoded version of the requested message text
  2686. */
  2687. function getMessageText($msgNo, $type, $structure, $fullHeader,$clean_email=true, $bcOffset = "") {
  2688. global $sugar_config;
  2689. $msgPart = '';
  2690. $bc = $this->buildBreadCrumbs($structure->parts, $type);
  2691. //Add an offset if specified
  2692. if(!empty($bcOffset))
  2693. $bc = $this->addBreadCrumbOffset($bc, $bcOffset);
  2694. if(!empty($bc)) { // multi-part
  2695. // HUGE difference between PLAIN and HTML
  2696. if($type == 'PLAIN') {
  2697. $msgPart = $this->getMessageTextFromSingleMimePart($msgNo,$bc,$structure);
  2698. } else {
  2699. // get part of structure that will
  2700. $msgPartRaw = '';
  2701. $bcArray = $this->buildBreadCrumbsHTML($structure->parts,$bcOffset);
  2702. // construct inline HTML/Rich msg
  2703. foreach($bcArray as $bcArryKey => $bcArr) {
  2704. foreach($bcArr as $type => $bcTrail) {
  2705. if($type == 'html')
  2706. $msgPartRaw .= $this->getMessageTextFromSingleMimePart($msgNo,$bcTrail,$structure);
  2707. else {
  2708. // deal with inline image
  2709. $part = $this->getPartByPath($bcTrail, $structure->parts);
  2710. if(empty($part) || empty($part->id)) continue;
  2711. $partid = substr($part->id, 1, -1); // strip <> around
  2712. if(isset($this->inlineImages[$partid])) {
  2713. $imageName = $this->inlineImages[$partid];
  2714. $newImagePath = "class=\"image\" src=\"{$this->imagePrefix}{$imageName}\"";
  2715. $preImagePath = "src=\"cid:$partid\"";
  2716. $msgPartRaw = str_replace($preImagePath, $newImagePath, $msgPartRaw);
  2717. }
  2718. }
  2719. }
  2720. }
  2721. $msgPart = $msgPartRaw;
  2722. }
  2723. } else { // either PLAIN message type (flowed) or b0rk3d RFC
  2724. // make sure we're working on valid data here.
  2725. if($structure->subtype != $type) {
  2726. return '';
  2727. }
  2728. $decodedHeader = $this->decodeHeader($fullHeader);
  2729. // now get actual body contents
  2730. $text = imap_body($this->conn, $msgNo);
  2731. $upperCaseKeyDecodeHeader = array();
  2732. if (is_array($decodedHeader)) {
  2733. $upperCaseKeyDecodeHeader = array_change_key_case($decodedHeader, CASE_UPPER);
  2734. } // if
  2735. if(isset($upperCaseKeyDecodeHeader[strtoupper('Content-Transfer-Encoding')])) {
  2736. $flip = array_flip($this->transferEncoding);
  2737. $text = $this->handleTranserEncoding($text, $flip[strtoupper($upperCaseKeyDecodeHeader[strtoupper('Content-Transfer-Encoding')])]);
  2738. }
  2739. if(is_array($upperCaseKeyDecodeHeader['CONTENT-TYPE']) && isset($upperCaseKeyDecodeHeader['CONTENT-TYPE']['charset']) && !empty($upperCaseKeyDecodeHeader['CONTENT-TYPE']['charset'])) {
  2740. // we have an explicit content type, use it
  2741. $msgPart = $this->handleCharsetTranslation($text, $upperCaseKeyDecodeHeader['CONTENT-TYPE']['charset']);
  2742. } else {
  2743. // make a best guess as to what our content type is
  2744. $msgPart = $this->convertToUtf8($text);
  2745. }
  2746. } // end else clause
  2747. $msgPart = $this->customGetMessageText($msgPart);
  2748. /* cn: bug 9176 - htmlEntitites hide XSS attacks. */
  2749. if($type == 'PLAIN') {
  2750. return SugarCleaner::cleanHtml(to_html($msgPart), false);
  2751. }
  2752. // Bug 50241: can't process <?xml:namespace .../> properly. Strip <?xml ...> tag first.
  2753. $msgPart = preg_replace("/<\?xml[^>]*>/","",$msgPart);
  2754. return SugarCleaner::cleanHtml($msgPart, false);
  2755. }
  2756. /**
  2757. * decodes raw header information and passes back an associative array with
  2758. * the important elements key'd by name
  2759. * @param header string the raw header
  2760. * @return decodedHeader array the associative array
  2761. */
  2762. function decodeHeader($fullHeader) {
  2763. $decodedHeader = array();
  2764. $exHeaders = explode("\r", $fullHeader);
  2765. if (!is_array($exHeaders)) {
  2766. $exHeaders = explode("\r\n", $fullHeader);
  2767. }
  2768. $quotes = array('"', "'");
  2769. foreach($exHeaders as $lineNum => $head) {
  2770. $key = '';
  2771. $key = trim(substr($head, 0, strpos($head, ':')));
  2772. $value = '';
  2773. $value = trim(substr($head, (strpos($head, ':') + 1), strlen($head)));
  2774. // handle content-type section in headers
  2775. if(strtolower($key) == 'content-type' && strpos($value, ';')) { // ";" means something follows related to (such as Charset)
  2776. $semiColPos = mb_strpos($value, ';');
  2777. $strLenVal = mb_strlen($value);
  2778. if(($semiColPos + 4) >= $strLenVal) {
  2779. // the charset="[something]" is on the next line
  2780. $value .= str_replace($quotes, "", trim($exHeaders[$lineNum+1]));
  2781. }
  2782. $newValue = array();
  2783. $exValue = explode(';', $value);
  2784. $newValue['type'] = $exValue[0];
  2785. for($i=1; $i<count($exValue); $i++) {
  2786. $exContent = explode('=', $exValue[$i]);
  2787. $newValue[trim($exContent[0])] = trim($exContent[1], "\t \"");
  2788. }
  2789. $value = $newValue;
  2790. }
  2791. if(!empty($key) && !empty($value)) {
  2792. $decodedHeader[$key] = $value;
  2793. }
  2794. }
  2795. return $decodedHeader;
  2796. }
  2797. /**
  2798. * handles translating message text from orignal encoding into UTF-8
  2799. *
  2800. * @param string text test to be re-encoded
  2801. * @param string charset original character set
  2802. * @return string utf8 re-encoded text
  2803. */
  2804. function handleCharsetTranslation($text, $charset) {
  2805. global $locale;
  2806. if(empty($charset)) {
  2807. $GLOBALS['log']->debug("***ERROR: InboundEmail::handleCharsetTranslation() called without a \$charset!");
  2808. $GLOBALS['log']->debug("***STACKTRACE: ".print_r(debug_backtrace(), true));
  2809. return $text;
  2810. }
  2811. // typical headers have no charset - let destination pick (since it's all ASCII anyways)
  2812. if(strtolower($charset) == 'default' || strtolower($charset) == 'utf-8') {
  2813. return $text;
  2814. }
  2815. return $locale->translateCharset($text, $charset);
  2816. }
  2817. /**
  2818. * Builds up the "breadcrumb" trail that imap_fetchbody() uses to return
  2819. * parts of an email message, including attachments and inline images
  2820. * @param $parts array of objects
  2821. * @param $subtype what type of trail to return? HTML? Plain? binaries?
  2822. * @param $breadcrumb text trail to build up
  2823. */
  2824. function buildBreadCrumbs($parts, $subtype, $breadcrumb = '0') {
  2825. //_pp('buildBreadCrumbs building for '.$subtype.' with BC at '.$breadcrumb);
  2826. // loop through available parts in the array
  2827. foreach($parts as $k => $part) {
  2828. // mark passage through level
  2829. $thisBc = ($k+1);
  2830. // if this is not the first time through, start building the map
  2831. if($breadcrumb != 0) {
  2832. $thisBc = $breadcrumb.'.'.$thisBc;
  2833. }
  2834. // found a multi-part/mixed 'part' - keep digging
  2835. if($part->type == 1 && (strtoupper($part->subtype) == 'RELATED' || strtoupper($part->subtype) == 'ALTERNATIVE' || strtoupper($part->subtype) == 'MIXED')) {
  2836. //_pp('in loop: going deeper with subtype: '.$part->subtype.' $k is: '.$k);
  2837. $thisBc = $this->buildBreadCrumbs($part->parts, $subtype, $thisBc);
  2838. return $thisBc;
  2839. } elseif(strtolower($part->subtype) == strtolower($subtype)) { // found the subtype we want, return the breadcrumb value
  2840. //_pp('found '.$subtype.' bc! returning: '.$thisBc);
  2841. return $thisBc;
  2842. } else {
  2843. //_pp('found '.$part->subtype.' instead');
  2844. }
  2845. }
  2846. }
  2847. /**
  2848. * Similar to buildBreadCrumbs() but returns an ordered array containing all parts of the message that would be
  2849. * considered "HTML" or Richtext (embedded images, formatting, etc.).
  2850. * @param array parts Array of parts of a message
  2851. * @param int breadcrumb Passed integer value to start breadcrumb trail
  2852. * @param array stackedBreadcrumbs Persistent trail of breadcrumbs
  2853. * @return array Ordered array of parts to retrieve via imap_fetchbody()
  2854. */
  2855. function buildBreadCrumbsHTML($parts, $breadcrumb = '0', $stackedBreadcrumbs = array()) {
  2856. $subtype = 'HTML';
  2857. $disposition = 'inline';
  2858. foreach($parts as $k => $part) {
  2859. // mark passage through level
  2860. $thisBc = ($k+1);
  2861. if($breadcrumb != 0) {
  2862. $thisBc = $breadcrumb.'.'.$thisBc;
  2863. }
  2864. // found a multi-part/mixed 'part' - keep digging
  2865. if($part->type == 1 && (strtoupper($part->subtype) == 'RELATED' || strtoupper($part->subtype) == 'ALTERNATIVE' || strtoupper($part->subtype) == 'MIXED')) {
  2866. $stackedBreadcrumbs = $this->buildBreadCrumbsHTML($part->parts, $thisBc, $stackedBreadcrumbs);
  2867. } elseif(
  2868. (strtolower($part->subtype) == strtolower($subtype)) ||
  2869. (
  2870. isset($part->disposition) && strtolower($part->disposition) == 'inline' &&
  2871. in_array(strtoupper($part->subtype), $this->imageTypes)
  2872. )
  2873. ) {
  2874. // found the subtype we want, return the breadcrumb value
  2875. $stackedBreadcrumbs[] = array(strtolower($part->subtype) => $thisBc);
  2876. } elseif($part->type == 5) {
  2877. $stackedBreadcrumbs[] = array(strtolower($part->subtype) => $thisBc);
  2878. }
  2879. }
  2880. return $stackedBreadcrumbs;
  2881. }
  2882. /**
  2883. * Takes a PHP imap_* object's to/from/cc/bcc address field and converts it
  2884. * to a standard string that SugarCRM expects
  2885. * @param $arr an array of email address objects
  2886. */
  2887. function convertImapToSugarEmailAddress($arr) {
  2888. if(is_array($arr)) {
  2889. $addr = '';
  2890. foreach($arr as $key => $obj) {
  2891. $addr .= $obj->mailbox.'@'.$obj->host.', ';
  2892. }
  2893. // strip last comma
  2894. $ret = substr_replace($addr,'',-2,-1);
  2895. return trim($ret);
  2896. }
  2897. }
  2898. /**
  2899. * tries to figure out what character set a given filename is using and
  2900. * decode based on that
  2901. *
  2902. * @param string name Name of attachment
  2903. * @return string decoded name
  2904. */
  2905. function handleEncodedFilename($name) {
  2906. $imapDecode = imap_mime_header_decode($name);
  2907. /******************************
  2908. $imapDecode => stdClass Object
  2909. (
  2910. [charset] => utf-8
  2911. [text] => w�hlen.php
  2912. )
  2913. OR
  2914. $imapDecode => stdClass Object
  2915. (
  2916. [charset] => default
  2917. [text] => UTF-8''%E3%83%8F%E3%82%99%E3%82%A4%E3%82%AA%E3%82%AF%E3%82%99%E3%83%A9%E3%83%95%E3%82%A3%E3%83%BC.txt
  2918. )
  2919. *******************************/
  2920. if($imapDecode[0]->charset != 'default') { // mime-header encoded charset
  2921. $encoding = $imapDecode[0]->charset;
  2922. $name = $imapDecode[0]->text; // encoded in that charset
  2923. } else {
  2924. /* encoded filenames are formatted as [encoding]''[filename] */
  2925. if(strpos($name, "''") !== false) {
  2926. $encoding = substr($name, 0, strpos($name, "'"));
  2927. while(strpos($name, "'") !== false) {
  2928. $name = trim(substr($name, (strpos($name, "'")+1), strlen($name)));
  2929. }
  2930. }
  2931. $name = urldecode($name);
  2932. }
  2933. return (strtolower($encoding) == 'utf-8') ? $name : $GLOBALS['locale']->translateCharset($name, $encoding, 'UTF-8');
  2934. }
  2935. /*
  2936. Primary body types for a part of a mail structure (imap_fetchstructure returned object)
  2937. 0 => text
  2938. 1 => multipart
  2939. 2 => message
  2940. 3 => application
  2941. 4 => audio
  2942. 5 => image
  2943. 6 => video
  2944. 7 => other
  2945. */
  2946. /**
  2947. Primary body types for a part of a mail structure (imap_fetchstructure returned object)
  2948. @var array $imap_types
  2949. */
  2950. public $imap_types = array(
  2951. 0 => 'text',
  2952. 1 => 'multipart',
  2953. 2 => 'message',
  2954. 3 => 'application',
  2955. 4 => 'audio',
  2956. 5 => 'image',
  2957. 6 => 'video',
  2958. );
  2959. public function getMimeType($type, $subtype)
  2960. {
  2961. if(isset($this->imap_types[$type])) {
  2962. return $this->imap_types[$type]."/$subtype";
  2963. } else {
  2964. return "other/$subtype";
  2965. }
  2966. }
  2967. /**
  2968. * Takes the "parts" attribute of the object that imap_fetchbody() method
  2969. * returns, and recursively goes through looking for objects that have a
  2970. * disposition of "attachement" or "inline"
  2971. * @param int $msgNo The relative message number for the monitored mailbox
  2972. * @param object $parts Array of objects to examine
  2973. * @param string $emailId The GUID of the email saved prior to calling this method
  2974. * @param array $breadcrumb Default 0, build up of the parts mapping
  2975. * @param bool $forDisplay Default false
  2976. */
  2977. function saveAttachments($msgNo, $parts, $emailId, $breadcrumb='0', $forDisplay) {
  2978. global $sugar_config;
  2979. /*
  2980. Primary body types for a part of a mail structure (imap_fetchstructure returned object)
  2981. 0 => text
  2982. 1 => multipart
  2983. 2 => message
  2984. 3 => application
  2985. 4 => audio
  2986. 5 => image
  2987. 6 => video
  2988. 7 => other
  2989. */
  2990. foreach($parts as $k => $part) {
  2991. $thisBc = $k+1;
  2992. if($breadcrumb != '0') {
  2993. $thisBc = $breadcrumb.'.'.$thisBc;
  2994. }
  2995. $attach = null;
  2996. // check if we need to recurse into the object
  2997. //if($part->type == 1 && !empty($part->parts)) {
  2998. if(isset($part->parts) && !empty($part->parts) && !( isset($part->subtype) && strtolower($part->subtype) == 'rfc822') ) {
  2999. $this->saveAttachments($msgNo, $part->parts, $emailId, $thisBc, $forDisplay);
  3000. continue;
  3001. } elseif($part->ifdisposition) {
  3002. // we will take either 'attachments' or 'inline'
  3003. if(strtolower($part->disposition) == 'attachment' || ((strtolower($part->disposition) == 'inline') && $part->type != 0)) {
  3004. $attach = $this->getNoteBeanForAttachment($emailId);
  3005. $fname = $this->handleEncodedFilename($this->retrieveAttachmentNameFromStructure($part));
  3006. if(!empty($fname)) {//assign name to attachment
  3007. $attach->name = $fname;
  3008. } else {//if name is empty, default to filename
  3009. $attach->name = urlencode($this->retrieveAttachmentNameFromStructure($part));
  3010. }
  3011. $attach->filename = $attach->name;
  3012. if (empty($attach->filename)) {
  3013. continue;
  3014. }
  3015. // deal with the MIME types email has
  3016. $attach->file_mime_type = $this->getMimeType($part->type, $part->subtype);
  3017. $attach->safeAttachmentName();
  3018. if($forDisplay) {
  3019. $attach->id = $this->getTempFilename();
  3020. } else {
  3021. // only save if doing a full import, else we want only the binaries
  3022. $attach->save();
  3023. }
  3024. } // end if disposition type 'attachment'
  3025. }// end ifdisposition
  3026. //Retrieve contents of subtype rfc8822
  3027. elseif ($part->type == 2 && isset($part->subtype) && strtolower($part->subtype) == 'rfc822' )
  3028. {
  3029. $tmp_eml = imap_fetchbody($this->conn, $msgNo, $thisBc);
  3030. $attach = $this->getNoteBeanForAttachment($emailId);
  3031. $attach->file_mime_type = 'messsage/rfc822';
  3032. $attach->description = $tmp_eml;
  3033. $attach->filename = 'bounce.eml';
  3034. $attach->safeAttachmentName();
  3035. if($forDisplay) {
  3036. $attach->id = $this->getTempFilename();
  3037. } else {
  3038. // only save if doing a full import, else we want only the binaries
  3039. $attach->save();
  3040. }
  3041. } elseif(!$part->ifdisposition && $part->type != 1 && $part->type != 2 && $thisBc != '1') {
  3042. // No disposition here, but some IMAP servers lie about disposition headers, try to find the truth
  3043. // Also Outlook puts inline attachments as type 5 (image) without a disposition
  3044. if($part->ifparameters) {
  3045. foreach($part->parameters as $param) {
  3046. if(strtolower($param->attribute) == "name" || strtolower($param->attribute) == "filename") {
  3047. $fname = $this->handleEncodedFilename($param->value);
  3048. break;
  3049. }
  3050. }
  3051. if(empty($fname)) continue;
  3052. // we assume that named parts are attachments too
  3053. $attach = $this->getNoteBeanForAttachment($emailId);
  3054. $attach->filename = $attach->name = $fname;
  3055. $attach->file_mime_type = $this->getMimeType($part->type, $part->subtype);
  3056. $attach->safeAttachmentName();
  3057. if($forDisplay) {
  3058. $attach->id = $this->getTempFilename();
  3059. } else {
  3060. // only save if doing a full import, else we want only the binaries
  3061. $attach->save();
  3062. }
  3063. }
  3064. }
  3065. $this->saveAttachmentBinaries($attach, $msgNo, $thisBc, $part, $forDisplay);
  3066. } // end foreach
  3067. }
  3068. /**
  3069. * Return a new note object for attachments.
  3070. *
  3071. * @param string $emailId
  3072. * @return Note
  3073. */
  3074. function getNoteBeanForAttachment($emailId)
  3075. {
  3076. $attach = new Note();
  3077. $attach->parent_id = $emailId;
  3078. $attach->parent_type = 'Emails';
  3079. return $attach;
  3080. }
  3081. /**
  3082. * Return the filename of the attachment by examining the dparameters or parameters returned from imap_fetch_structure
  3083. *
  3084. * @param object $part
  3085. * @return string
  3086. */
  3087. function retrieveAttachmentNameFromStructure($part)
  3088. {
  3089. $result = "";
  3090. foreach ($part->dparamaters as $k => $v)
  3091. {
  3092. if( strtolower($v->attribute) == 'filename')
  3093. {
  3094. $result = $v->value;
  3095. break;
  3096. }
  3097. }
  3098. if (empty($result)) {
  3099. foreach ($part->parameters as $k => $v) {
  3100. if (strtolower($v->attribute) == 'name') {
  3101. $result = $v->value;
  3102. break;
  3103. }
  3104. }
  3105. }
  3106. return $result;
  3107. }
  3108. /**
  3109. * saves the actual binary file of a given attachment
  3110. * @param object attach Note object that is attached to the binary file
  3111. * @param string msgNo Message Number on IMAP/POP3 server
  3112. * @param string thisBc Breadcrumb to navigate email structure to find the content
  3113. * @param object part IMAP standard object that contains the "parts" of this section of email
  3114. * @param bool $forDisplay
  3115. */
  3116. function saveAttachmentBinaries($attach, $msgNo, $thisBc, $part, $forDisplay) {
  3117. // decide where to place the file temporarily
  3118. $uploadDir = ($forDisplay) ? "{$this->EmailCachePath}/{$this->id}/attachments/" : "upload://";
  3119. // decide what name to save file as
  3120. $fileName = $attach->id;
  3121. // download the attachment if we didn't do it yet
  3122. if(!file_exists($uploadDir.$fileName)) {
  3123. $msgPartRaw = imap_fetchbody($this->conn, $msgNo, $thisBc);
  3124. // deal with attachment encoding and decode the text string
  3125. $msgPart = $this->handleTranserEncoding($msgPartRaw, $part->encoding);
  3126. if(file_put_contents($uploadDir.$fileName, $msgPart)) {
  3127. $GLOBALS['log']->debug('InboundEmail saved attachment file: '.$attach->filename);
  3128. } else {
  3129. $GLOBALS['log']->debug('InboundEmail could not create attachment file: '.$attach->filename ." - temp file target: [ {$uploadDir}{$fileName} ]");
  3130. return;
  3131. }
  3132. }
  3133. $this->tempAttachment[$fileName] = urldecode($attach->filename);
  3134. // if all was successful, feel for inline and cache Note ID for display:
  3135. if((strtolower($part->disposition) == 'inline' && in_array($part->subtype, $this->imageTypes))
  3136. || ($part->type == 5)) {
  3137. if(copy($uploadDir.$fileName, sugar_cached("images/{$fileName}.").strtolower($part->subtype))) {
  3138. $id = substr($part->id, 1, -1); //strip <> around
  3139. $this->inlineImages[$id] = $attach->id.".".strtolower($part->subtype);
  3140. } else {
  3141. $GLOBALS['log']->debug('InboundEmail could not copy '.$uploadDir.$fileName.' to cache');
  3142. }
  3143. }
  3144. }
  3145. /**
  3146. * decodes a string based on its associated encoding
  3147. * if nothing is passed, we default to no-encoding type
  3148. * @param $str encoded string
  3149. * @param $enc detected encoding
  3150. */
  3151. function handleTranserEncoding($str, $enc=0) {
  3152. switch($enc) {
  3153. case 2:// BINARY
  3154. $ret = $str;
  3155. break;
  3156. case 3:// BASE64
  3157. $ret = base64_decode($str);
  3158. break;
  3159. case 4:// QUOTED-PRINTABLE
  3160. $ret = quoted_printable_decode($str);
  3161. break;
  3162. case 0:// 7BIT or 8BIT
  3163. case 1:// already in a string-useable format - do nothing
  3164. case 5:// OTHER
  3165. default:// catch all
  3166. $ret = $str;
  3167. break;
  3168. }
  3169. return $ret;
  3170. }
  3171. /**
  3172. * Some emails do not get assigned a message_id, specifically from
  3173. * Outlook/Exchange.
  3174. *
  3175. * We need to derive a reliable one for duplicate import checking.
  3176. */
  3177. function getMessageId($header) {
  3178. $message_id = md5(print_r($header, true));
  3179. return $message_id;
  3180. }
  3181. /**
  3182. * checks for duplicate emails on polling. The uniqueness of a given email message is determined by a concatenation
  3183. * of 2 values, the messageID and the delivered-to field. This allows multiple To: and B/CC: destination addresses
  3184. * to be imported by Sugar without violating the true duplicate-email issues.
  3185. *
  3186. * @param string message_id message ID generated by sending server
  3187. * @param int message number (mailserver's key) of email
  3188. * @param object header object generated by imap_headerinfo()
  3189. * @param string textHeader Headers in normal text format
  3190. * @return bool
  3191. */
  3192. function importDupeCheck($message_id, $header, $textHeader) {
  3193. $GLOBALS['log']->debug('*********** InboundEmail doing dupe check.');
  3194. // generate "delivered-to" seed for email duplicate check
  3195. $deliveredTo = $this->id; // cn: bug 12236 - cc's failing dupe check
  3196. $exHeader = explode("\n", $textHeader);
  3197. foreach($exHeader as $headerLine) {
  3198. if(strpos(strtolower($headerLine), 'delivered-to:') !== false) {
  3199. $deliveredTo = substr($headerLine, strpos($headerLine, " "), strlen($headerLine));
  3200. $GLOBALS['log']->debug('********* InboundEmail found [ '.$deliveredTo.' ] as the destination address for email [ '.$message_id.' ]');
  3201. } elseif(strpos(strtolower($headerLine), 'x-real-to:') !== false) {
  3202. $deliveredTo = substr($headerLine, strpos($headerLine, " "), strlen($headerLine));
  3203. $GLOBALS['log']->debug('********* InboundEmail found [ '.$deliveredTo.' ] for non-standards compliant email x-header [ '.$message_id.' ]');
  3204. }
  3205. }
  3206. //if(empty($message_id) && !isset($message_id)) {
  3207. if(empty($message_id) || !isset($message_id)) {
  3208. $GLOBALS['log']->debug('*********** NO MESSAGE_ID.');
  3209. $message_id = $this->getMessageId($header);
  3210. }
  3211. // generate compound messageId
  3212. $this->compoundMessageId = trim($message_id).trim($deliveredTo);
  3213. if (empty($this->compoundMessageId)) {
  3214. $GLOBALS['log']->error('Inbound Email found a message without a header and message_id');
  3215. return false;
  3216. } // if
  3217. $this->compoundMessageId = md5($this->compoundMessageId);
  3218. $query = 'SELECT count(emails.id) AS c FROM emails WHERE emails.message_id = \''.$this->compoundMessageId.'\' and emails.deleted = 0';
  3219. $r = $this->db->query($query, true);
  3220. $a = $this->db->fetchByAssoc($r);
  3221. if($a['c'] > 0) {
  3222. $GLOBALS['log']->debug('InboundEmail found a duplicate email with ID ('.$this->compoundMessageId.')');
  3223. return false; // we have a dupe and don't want to import the email'
  3224. } else {
  3225. return true;
  3226. }
  3227. }
  3228. /**
  3229. * takes the output from imap_mime_hader_decode() and handles multiple types of encoding
  3230. * @param string subject Raw subject string from email
  3231. * @return string ret properly formatted UTF-8 string
  3232. */
  3233. function handleMimeHeaderDecode($subject) {
  3234. $subjectDecoded = imap_mime_header_decode($subject);
  3235. $ret = '';
  3236. foreach($subjectDecoded as $object) {
  3237. if($object->charset != 'default') {
  3238. $ret .= $this->handleCharsetTranslation($object->text, $object->charset);
  3239. } else {
  3240. $ret .= $object->text;
  3241. }
  3242. }
  3243. return $ret;
  3244. }
  3245. /**
  3246. * Calculates the appropriate display date/time sent for an email.
  3247. * @param string headerDate The date sent of email in MIME header format
  3248. * @return string GMT-0 Unix timestamp
  3249. */
  3250. function getUnixHeaderDate($headerDate) {
  3251. global $timedate;
  3252. if (empty($headerDate)) {
  3253. return "";
  3254. }
  3255. ///////////////////////////////////////////////////////////////////
  3256. //// CALCULATE CORRECT SENT DATE/TIME FOR EMAIL
  3257. if(!empty($headerDate)) {
  3258. // Bug 25254 - Strip trailing space that come in some header dates (maybe ones with 1-digit day number)
  3259. $headerDate = trim($headerDate);
  3260. // need to hack PHP/windows' bad handling of strings when using POP3
  3261. if(strstr($headerDate,'+0000 GMT')) {
  3262. $headerDate = str_replace('GMT','', $headerDate);
  3263. } elseif(!strtotime($headerDate)) {
  3264. $headerDate = 'now'; // catch non-standard format times.
  3265. } else {
  3266. // cn: bug 9196 parse the GMT offset
  3267. if(strpos($headerDate, '-') || strpos($headerDate, '+')) {
  3268. // cn: bug make sure last 5 chars are [+|-]nnnn
  3269. if(strpos($headerDate, "(")) {
  3270. $headerDate = preg_replace('/\([\w]+\)/i', "", $headerDate);
  3271. $headerDate = trim($headerDate);
  3272. }
  3273. // parse mailserver time
  3274. $gmtEmail = trim(substr($headerDate, -5, 5));
  3275. $posNeg = substr($gmtEmail, 0, 1);
  3276. $gmtHours = substr($gmtEmail, 1, 2);
  3277. $gmtMins = substr($gmtEmail, -2, 2);
  3278. // get seconds
  3279. $secsHours = $gmtHours * 60 * 60;
  3280. $secsTotal = $secsHours + ($gmtMins * 60);
  3281. $secsTotal = ($posNeg == '-') ? $secsTotal : -1 * $secsTotal;
  3282. $headerDate = trim(substr_replace($headerDate, '', -5)); // mfh: bug 10961/12855 - date time values with GMT offsets not properly formatted
  3283. }
  3284. }
  3285. } else {
  3286. $headerDate = 'now';
  3287. }
  3288. $unixHeaderDate = strtotime($headerDate);
  3289. if(isset($secsTotal)) {
  3290. // this gets the timestamp to true GMT-0
  3291. $unixHeaderDate += $secsTotal;
  3292. }
  3293. if(strtotime('Jan 1, 2001') > $unixHeaderDate) {
  3294. $unixHeaderDate = strtotime('now');
  3295. }
  3296. return $unixHeaderDate;
  3297. //// END CALCULATE CORRECT SENT DATE/TIME FOR EMAIL
  3298. ///////////////////////////////////////////////////////////////////
  3299. }
  3300. /**
  3301. * This method returns the correct messageno for the pop3 protocol
  3302. * @param String UIDL
  3303. * @return returnMsgNo
  3304. */
  3305. function getCorrectMessageNoForPop3($messageId) {
  3306. $returnMsgNo = -1;
  3307. if ($this->protocol == 'pop3') {
  3308. if($this->pop3_open()) {
  3309. // get the UIDL from database;
  3310. $query = "SELECT msgno FROM email_cache WHERE ie_id = '{$this->id}' AND message_id = '{$messageId}'";
  3311. $r = $this->db->query($query);
  3312. $a = $this->db->fetchByAssoc($r);
  3313. $msgNo = $a['msgno'];
  3314. $returnMsgNo = $msgNo;
  3315. // authenticate
  3316. $this->pop3_sendCommand("USER", $this->email_user);
  3317. $this->pop3_sendCommand("PASS", $this->email_password);
  3318. // get UIDL for this msgNo
  3319. $this->pop3_sendCommand("UIDL {$msgNo}", '', false); // leave socket buffer alone until the while()
  3320. $buf = fgets($this->pop3socket, 1024); // handle "OK+ msgNo UIDL(UIDL for this messageno)";
  3321. // if it returns OK then we have found the message else get all the UIDL
  3322. // and search for the correct msgNo;
  3323. $foundMessageNo = false;
  3324. if (preg_match("/OK/", $buf) > 0) {
  3325. $mailserverResponse = explode(" ", $buf);
  3326. // if the cachedUIDL and the UIDL from mail server matches then its the correct messageno
  3327. if (trim($mailserverResponse[sizeof($mailserverResponse) - 1]) == $messageId) {
  3328. $foundMessageNo = true;
  3329. }
  3330. } //if
  3331. //get all the UIDL and then find the correct messageno
  3332. if (!$foundMessageNo) {
  3333. // get UIDLs
  3334. $this->pop3_sendCommand("UIDL", '', false); // leave socket buffer alone until the while()
  3335. fgets($this->pop3socket, 1024); // handle "OK+";
  3336. $UIDLs = array();
  3337. $buf = '!';
  3338. if(is_resource($this->pop3socket)) {
  3339. while(!feof($this->pop3socket)) {
  3340. $buf = fgets($this->pop3socket, 1024); // 8kb max buffer - shouldn't be more than 80 chars via pop3...
  3341. if(trim($buf) == '.') {
  3342. $GLOBALS['log']->debug("*** GOT '.'");
  3343. break;
  3344. } // if
  3345. // format is [msgNo] [UIDL]
  3346. $exUidl = explode(" ", $buf);
  3347. $UIDLs[trim($exUidl[1])] = trim($exUidl[0]);
  3348. } // while
  3349. if (array_key_exists($messageId, $UIDLs)) {
  3350. $returnMsgNo = $UIDLs[$messageId];
  3351. } else {
  3352. // message could not be found on server
  3353. $returnMsgNo = -1;
  3354. } // else
  3355. } // if
  3356. } // if
  3357. $this->pop3_cleanUp();
  3358. } //if
  3359. } //if
  3360. return $returnMsgNo;
  3361. }
  3362. /**
  3363. * If the importOneEmail returns false, then findout if the duplicate email
  3364. */
  3365. function getDuplicateEmailId($msgNo, $uid) {
  3366. global $timedate;
  3367. global $app_strings;
  3368. global $app_list_strings;
  3369. global $sugar_config;
  3370. global $current_user;
  3371. $header = imap_headerinfo($this->conn, $msgNo);
  3372. $fullHeader = imap_fetchheader($this->conn, $msgNo); // raw headers
  3373. // reset inline images cache
  3374. $this->inlineImages = array();
  3375. // handle messages deleted on server
  3376. if(empty($header)) {
  3377. if(!isset($this->email) || empty($this->email)) {
  3378. $this->email = new Email();
  3379. } // if
  3380. return "";
  3381. } else {
  3382. $dupeCheckResult = $this->importDupeCheck($header->message_id, $header, $fullHeader);
  3383. if (!$dupeCheckResult && !empty($this->compoundMessageId)) {
  3384. // we have a duplicate email
  3385. $query = 'SELECT id FROM emails WHERE emails.message_id = \''.$this->compoundMessageId.'\' and emails.deleted = 0';
  3386. $r = $this->db->query($query, true);
  3387. $a = $this->db->fetchByAssoc($r);
  3388. $this->email = new Email();
  3389. $this->email->id = $a['id'];
  3390. return $a['id'];
  3391. } // if
  3392. return "";
  3393. } // else
  3394. } // fn
  3395. /**
  3396. * shiny new importOneEmail() method
  3397. * @param int msgNo
  3398. * @param bool forDisplay
  3399. * @param clean_email boolean, default true,
  3400. */
  3401. function importOneEmail($msgNo, $uid, $forDisplay=false, $clean_email=true) {
  3402. $GLOBALS['log']->debug("InboundEmail processing 1 email {$msgNo}-----------------------------------------------------------------------------------------");
  3403. global $timedate;
  3404. global $app_strings;
  3405. global $app_list_strings;
  3406. global $sugar_config;
  3407. global $current_user;
  3408. // Bug # 45477
  3409. // So, on older versions of PHP (PHP VERSION < 5.3),
  3410. // calling imap_headerinfo and imap_fetchheader can cause a buffer overflow for exteremly large headers,
  3411. // This leads to the remaining messages not being read because Sugar crashes everytime it tries to read the headers.
  3412. // The workaround is to mark a message as read before making trying to read the header of the msg in question
  3413. // This forces this message not be read again, and we can continue processing remaining msgs.
  3414. // UNCOMMENT THIS IF YOU HAVE THIS PROBLEM! See notes on Bug # 45477
  3415. // $this->markEmails($uid, "read");
  3416. $header = imap_headerinfo($this->conn, $msgNo);
  3417. $fullHeader = imap_fetchheader($this->conn, $msgNo); // raw headers
  3418. // reset inline images cache
  3419. $this->inlineImages = array();
  3420. // handle messages deleted on server
  3421. if(empty($header)) {
  3422. if(!isset($this->email) || empty($this->email)) {
  3423. $this->email = new Email();
  3424. }
  3425. $q = "";
  3426. if ($this->isPop3Protocol()) {
  3427. $this->email->name = $app_strings['LBL_EMAIL_ERROR_MESSAGE_DELETED'];
  3428. $q = "DELETE FROM email_cache WHERE message_id = '{$uid}' AND ie_id = '{$this->id}' AND mbox = '{$this->mailbox}'";
  3429. } else {
  3430. $this->email->name = $app_strings['LBL_EMAIL_ERROR_IMAP_MESSAGE_DELETED'];
  3431. $q = "DELETE FROM email_cache WHERE imap_uid = {$uid} AND ie_id = '{$this->id}' AND mbox = '{$this->mailbox}'";
  3432. } // else
  3433. // delete local cache
  3434. $r = $this->db->query($q);
  3435. $this->email->date_sent = $timedate->nowDb();
  3436. return false;
  3437. //return "Message deleted from server.";
  3438. }
  3439. ///////////////////////////////////////////////////////////////////////
  3440. //// DUPLICATE CHECK
  3441. $dupeCheckResult = $this->importDupeCheck($header->message_id, $header, $fullHeader);
  3442. if($forDisplay || $dupeCheckResult) {
  3443. $GLOBALS['log']->debug('*********** NO duplicate found, continuing with processing.');
  3444. $structure = imap_fetchstructure($this->conn, $msgNo); // map of email
  3445. ///////////////////////////////////////////////////////////////////
  3446. //// CREATE SEED EMAIL OBJECT
  3447. $email = new Email();
  3448. $email->isDuplicate = ($dupeCheckResult) ? false : true;
  3449. $email->mailbox_id = $this->id;
  3450. $message = array();
  3451. $email->id = create_guid();
  3452. $email->new_with_id = true; //forcing a GUID here to prevent double saves.
  3453. //// END CREATE SEED EMAIL
  3454. ///////////////////////////////////////////////////////////////////
  3455. ///////////////////////////////////////////////////////////////////
  3456. //// PREP SYSTEM USER
  3457. if(empty($current_user)) {
  3458. // I-E runs as admin, get admin prefs
  3459. $current_user = new User();
  3460. $current_user->getSystemUser();
  3461. }
  3462. $tPref = $current_user->getUserDateTimePreferences();
  3463. //// END USER PREP
  3464. ///////////////////////////////////////////////////////////////////
  3465. if(!empty($header->date)) {
  3466. $unixHeaderDate = $timedate->fromString($header->date);
  3467. }
  3468. ///////////////////////////////////////////////////////////////////
  3469. //// HANDLE EMAIL ATTACHEMENTS OR HTML TEXT
  3470. //// Inline images require that I-E handle attachments before body text
  3471. // parts defines attachments - be mindful of .html being interpreted as an attachment
  3472. if($structure->type == 1 && !empty($structure->parts)) {
  3473. $GLOBALS['log']->debug('InboundEmail found multipart email - saving attachments if found.');
  3474. $this->saveAttachments($msgNo, $structure->parts, $email->id, 0, $forDisplay);
  3475. } elseif($structure->type == 0) {
  3476. $uuemail = ($this->isUuencode($email->description)) ? true : false;
  3477. /*
  3478. * UUEncoded attachments - legacy, but still have to deal with it
  3479. * format:
  3480. * begin 777 filename.txt
  3481. * UUENCODE
  3482. *
  3483. * end
  3484. */
  3485. // set body to the filtered one
  3486. if($uuemail) {
  3487. $email->description = $this->handleUUEncodedEmailBody($email->description, $email->id);
  3488. $email->retrieve($email->id);
  3489. $email->save();
  3490. }
  3491. } else {
  3492. if($this->port != 110) {
  3493. $GLOBALS['log']->debug('InboundEmail found a multi-part email (id:'.$msgNo.') with no child parts to parse.');
  3494. }
  3495. }
  3496. //// END HANDLE EMAIL ATTACHEMENTS OR HTML TEXT
  3497. ///////////////////////////////////////////////////////////////////
  3498. ///////////////////////////////////////////////////////////////////
  3499. //// ASSIGN APPROPRIATE ATTRIBUTES TO NEW EMAIL OBJECT
  3500. // handle UTF-8/charset encoding in the ***headers***
  3501. global $db;
  3502. $email->name = $this->handleMimeHeaderDecode($header->subject);
  3503. $email->type = 'inbound';
  3504. if(!empty($unixHeaderDate)) {
  3505. $email->date_sent = $timedate->asUser($unixHeaderDate);
  3506. list($email->date_start, $email->time_start) = $timedate->split_date_time($email->date_sent);
  3507. } else {
  3508. $email->date_start = $email->time_start = $email->date_sent = "";
  3509. }
  3510. $email->status = 'unread'; // this is used in Contacts' Emails SubPanel
  3511. if(!empty($header->toaddress)) {
  3512. $email->to_name = $this->handleMimeHeaderDecode($header->toaddress);
  3513. $email->to_addrs_names = $email->to_name;
  3514. }
  3515. if(!empty($header->to)) {
  3516. $email->to_addrs = $this->convertImapToSugarEmailAddress($header->to);
  3517. }
  3518. $email->from_name = $this->handleMimeHeaderDecode($header->fromaddress);
  3519. $email->from_addr_name = $email->from_name;
  3520. $email->from_addr = $this->convertImapToSugarEmailAddress($header->from);
  3521. if(!empty($header->cc)) {
  3522. $email->cc_addrs = $this->convertImapToSugarEmailAddress($header->cc);
  3523. }
  3524. if(!empty($header->ccaddress)) {
  3525. $email->cc_addrs_names = $this->handleMimeHeaderDecode($header->ccaddress);
  3526. } // if
  3527. $email->reply_to_name = $this->handleMimeHeaderDecode($header->reply_toaddress);
  3528. $email->reply_to_email = $this->convertImapToSugarEmailAddress($header->reply_to);
  3529. if (!empty($email->reply_to_email)) {
  3530. $email->reply_to_addr = $email->reply_to_name;
  3531. }
  3532. $email->intent = $this->mailbox_type;
  3533. $email->message_id = $this->compoundMessageId; // filled by importDupeCheck();
  3534. $oldPrefix = $this->imagePrefix;
  3535. if(!$forDisplay) {
  3536. // Store CIDs in imported messages, convert on display
  3537. $this->imagePrefix = "cid:";
  3538. }
  3539. // handle multi-part email bodies
  3540. $email->description_html= $this->getMessageText($msgNo, 'HTML', $structure, $fullHeader,$clean_email); // runs through handleTranserEncoding() already
  3541. $email->description = $this->getMessageText($msgNo, 'PLAIN', $structure, $fullHeader,$clean_email); // runs through handleTranserEncoding() already
  3542. $this->imagePrefix = $oldPrefix;
  3543. // empty() check for body content
  3544. if(empty($email->description)) {
  3545. $GLOBALS['log']->debug('InboundEmail Message (id:'.$email->message_id.') has no body');
  3546. }
  3547. // assign_to group
  3548. if (!empty($_REQUEST['user_id'])) {
  3549. $email->assigned_user_id = $_REQUEST['user_id'];
  3550. } else {
  3551. // Samir Gandhi : Commented out this code as its not needed
  3552. //$email->assigned_user_id = $this->group_id;
  3553. }
  3554. //Assign Parent Values if set
  3555. if (!empty($_REQUEST['parent_id']) && !empty($_REQUEST['parent_type'])) {
  3556. $email->parent_id = $_REQUEST['parent_id'];
  3557. $email->parent_type = $_REQUEST['parent_type'];
  3558. $mod = strtolower($email->parent_type);
  3559. $rel = array_key_exists($mod, $email->field_defs) ? $mod : $mod . "_activities_emails"; //Custom modules rel name
  3560. if(! $email->load_relationship($rel) )
  3561. return FALSE;
  3562. $email->$rel->add($email->parent_id);
  3563. }
  3564. // override $forDisplay w/user pref
  3565. if($forDisplay) {
  3566. if($this->isAutoImport()) {
  3567. $forDisplay = false; // triggers save of imported email
  3568. }
  3569. }
  3570. if(!$forDisplay) {
  3571. $email->save();
  3572. $email->new_with_id = false; // to allow future saves by UPDATE, instead of INSERT
  3573. //// ASSIGN APPROPRIATE ATTRIBUTES TO NEW EMAIL OBJECT
  3574. ///////////////////////////////////////////////////////////////////
  3575. ///////////////////////////////////////////////////////////////////
  3576. //// LINK APPROPRIATE BEANS TO NEWLY SAVED EMAIL
  3577. //$contactAddr = $this->handleLinking($email);
  3578. //// END LINK APPROPRIATE BEANS TO NEWLY SAVED EMAIL
  3579. ///////////////////////////////////////////////////////////////////
  3580. ///////////////////////////////////////////////////////////////////
  3581. //// MAILBOX TYPE HANDLING
  3582. $this->handleMailboxType($email, $header);
  3583. //// END MAILBOX TYPE HANDLING
  3584. ///////////////////////////////////////////////////////////////////
  3585. ///////////////////////////////////////////////////////////////////
  3586. //// SEND AUTORESPONSE
  3587. if(!empty($email->reply_to_email)) {
  3588. $contactAddr = $email->reply_to_email;
  3589. } else {
  3590. $contactAddr = $email->from_addr;
  3591. }
  3592. if (!$this->isMailBoxTypeCreateCase()) {
  3593. $this->handleAutoresponse($email, $contactAddr);
  3594. }
  3595. //// END SEND AUTORESPONSE
  3596. ///////////////////////////////////////////////////////////////////
  3597. //// END IMPORT ONE EMAIL
  3598. ///////////////////////////////////////////////////////////////////
  3599. }
  3600. } else {
  3601. // only log if not POP3; pop3 iterates through ALL mail
  3602. if($this->protocol != 'pop3') {
  3603. $GLOBALS['log']->info("InboundEmail found a duplicate email: ".$header->message_id);
  3604. //echo "This email has already been imported";
  3605. }
  3606. return false;
  3607. }
  3608. //// END DUPLICATE CHECK
  3609. ///////////////////////////////////////////////////////////////////////
  3610. ///////////////////////////////////////////////////////////////////////
  3611. //// DEAL WITH THE MAILBOX
  3612. if(!$forDisplay) {
  3613. $r = imap_setflag_full($this->conn, $msgNo, '\\SEEN');
  3614. // if delete_seen, mark msg as deleted
  3615. if($this->delete_seen == 1 && !$forDisplay) {
  3616. $GLOBALS['log']->info("INBOUNDEMAIL: delete_seen == 1 - deleting email");
  3617. imap_setflag_full($this->conn, $msgNo, '\\DELETED');
  3618. }
  3619. } else {
  3620. // for display - don't touch server files?
  3621. //imap_setflag_full($this->conn, $msgNo, '\\UNSEEN');
  3622. }
  3623. $GLOBALS['log']->debug('********************************* InboundEmail finished import of 1 email: '.$email->name);
  3624. //// END DEAL WITH THE MAILBOX
  3625. ///////////////////////////////////////////////////////////////////////
  3626. ///////////////////////////////////////////////////////////////////////
  3627. //// TO SUPPORT EMAIL 2.0
  3628. $this->email = $email;
  3629. if(empty($this->email->et)) {
  3630. $this->email->email2init();
  3631. }
  3632. return true;
  3633. }
  3634. /**
  3635. * figures out if a plain text email body has UUEncoded attachments
  3636. * @param string string The email body
  3637. * @return bool True if UUEncode is detected.
  3638. */
  3639. function isUuencode($string) {
  3640. $rx = "begin [0-9]{3} .*";
  3641. $exBody = explode("\r", $string);
  3642. foreach($exBody as $line) {
  3643. if(preg_match("/begin [0-9]{3} .*/i", $line)) {
  3644. return true;
  3645. }
  3646. }
  3647. return false;
  3648. }
  3649. /**
  3650. * handles UU Encoded emails - a legacy from pre-RFC 822 which must still be supported (?)
  3651. * @param string raw The raw email body
  3652. * @param string id Parent email ID
  3653. * @return string The filtered email body, stripped of attachments
  3654. */
  3655. function handleUUEncodedEmailBody($raw, $id) {
  3656. global $locale;
  3657. $emailBody = '';
  3658. $attachmentBody = '';
  3659. $inAttachment = false;
  3660. $exRaw = explode("\n", $raw);
  3661. foreach($exRaw as $k => $line) {
  3662. $line = trim($line);
  3663. if(preg_match("/begin [0-9]{3} .*/i", $line, $m)) {
  3664. $inAttachment = true;
  3665. $fileName = $this->handleEncodedFilename(substr($m[0], 10, strlen($m[0])));
  3666. $attachmentBody = ''; // reset for next part of loop;
  3667. continue;
  3668. }
  3669. // handle "end"
  3670. if(strpos($line, "end") === 0) {
  3671. if(!empty($fileName) && !empty($attachmentBody)) {
  3672. $this->handleUUDecode($id, $fileName, trim($attachmentBody));
  3673. $attachmentBody = ''; // reset for next part of loop;
  3674. }
  3675. }
  3676. if($inAttachment === false) {
  3677. $emailBody .= "\n".$line;
  3678. } else {
  3679. $attachmentBody .= "\n".$line;
  3680. }
  3681. }
  3682. /* since UUEncode was developed before MIME, we have NO idea what character set encoding was used. we will assume the user's locale character set */
  3683. $emailBody = $locale->translateCharset($emailBody, $locale->getExportCharset(), 'UTF-8');
  3684. return $emailBody;
  3685. }
  3686. /**
  3687. * wrapper for UUDecode
  3688. * @param string id Id of the email
  3689. * @param string UUEncode Encode US-ASCII
  3690. */
  3691. function handleUUDecode($id, $fileName, $UUEncode) {
  3692. global $sugar_config;
  3693. /* include PHP_Compat library; it auto-feels for PHP5's compiled convert_uuencode() function */
  3694. require_once('include/PHP_Compat/convert_uudecode.php');
  3695. $attach = new Note();
  3696. $attach->parent_id = $id;
  3697. $attach->parent_type = 'Emails';
  3698. $fname = $this->handleEncodedFilename($fileName);
  3699. if(!empty($fname)) {//assign name to attachment
  3700. $attach->name = $fname;
  3701. } else {//if name is empty, default to filename
  3702. $attach->name = urlencode($fileName);
  3703. }
  3704. $attach->filename = urlencode($attach->name);
  3705. //get position of last "." in file name
  3706. $file_ext_beg = strrpos($attach->filename,".");
  3707. $file_ext = "";
  3708. //get file extension
  3709. if($file_ext_beg >0) {
  3710. $file_ext = substr($attach->filename, $file_ext_beg+1);
  3711. }
  3712. //check to see if this is a file with extension located in "badext"
  3713. foreach($sugar_config['upload_badext'] as $badExt) {
  3714. if(strtolower($file_ext) == strtolower($badExt)) {
  3715. //if found, then append with .txt and break out of lookup
  3716. $attach->name = $attach->name . ".txt";
  3717. $attach->file_mime_type = 'text/';
  3718. $attach->filename = $attach->filename . ".txt";
  3719. break; // no need to look for more
  3720. }
  3721. }
  3722. $attach->save();
  3723. $bin = convert_uudecode($UUEncode);
  3724. $filename = "upload://{$attach->id}";
  3725. if(file_put_contents($filename, $bin)) {
  3726. $GLOBALS['log']->debug('InboundEmail saved attachment file: '.$filename);
  3727. } else {
  3728. $GLOBALS['log']->debug('InboundEmail could not create attachment file: '.$filename);
  3729. }
  3730. }
  3731. /**
  3732. * returns true if the email's domain is NOT in the filter domain string
  3733. *
  3734. * @param object email Email object in question
  3735. * @return bool true if not filtered, false if filtered
  3736. */
  3737. function checkFilterDomain($email) {
  3738. $filterDomain = $this->get_stored_options('filter_domain');
  3739. if(!isset($filterDomain) || empty($filterDomain)) {
  3740. return true; // nothing set for this
  3741. } else {
  3742. $replyTo = strtolower($email->reply_to_email);
  3743. $from = strtolower($email->from_addr);
  3744. $filterDomain = '@'.strtolower($filterDomain);
  3745. if(strpos($replyTo, $filterDomain) !== false) {
  3746. $GLOBALS['log']->debug('Autoreply cancelled - [reply to] address domain matches filter domain.');
  3747. return false;
  3748. } elseif(strpos($from, $filterDomain) !== false) {
  3749. $GLOBALS['log']->debug('Autoreply cancelled - [from] address domain matches filter domain.');
  3750. return false;
  3751. } else {
  3752. return true; // no match
  3753. }
  3754. }
  3755. }
  3756. /**
  3757. * returns true if subject is NOT "out of the office" type
  3758. *
  3759. * @param string subject Subject line of email in question
  3760. * @return bool returns false if OOTO found
  3761. */
  3762. function checkOutOfOffice($subject) {
  3763. $ooto = array("Out of the Office", "Out of Office");
  3764. foreach($ooto as $str) {
  3765. if(preg_match('/'.$str.'/i', $subject)) {
  3766. $GLOBALS['log']->debug('Autoreply cancelled - found "Out of Office" type of subject.');
  3767. return false;
  3768. }
  3769. }
  3770. return true; // no matches to ooto strings
  3771. }
  3772. /**
  3773. * sets a timestamp for an autoreply to a single email addy
  3774. *
  3775. * @param string addr Address of auto-replied target
  3776. */
  3777. function setAutoreplyStatus($addr) {
  3778. $timedate = TimeDate::getInstance();
  3779. $this->db->query( 'INSERT INTO inbound_email_autoreply (id, deleted, date_entered, date_modified, autoreplied_to, ie_id) VALUES (
  3780. \''.create_guid().'\',
  3781. 0,
  3782. \''.$timedate->nowDb().'\',
  3783. \''.$timedate->nowDb().'\',
  3784. \''.$addr.'\',
  3785. \''.$this->id.'\') ', true);
  3786. }
  3787. /**
  3788. * returns true if recipient has NOT received 10 auto-replies in 24 hours
  3789. *
  3790. * @param string from target address for auto-reply
  3791. * @return bool true if target is valid/under limit
  3792. */
  3793. function getAutoreplyStatus($from) {
  3794. global $sugar_config;
  3795. $timedate = TimeDate::getInstance();
  3796. $q_clean = 'UPDATE inbound_email_autoreply SET deleted = 1 WHERE date_entered < \''.$timedate->getNow()->modify("-24 hours")->asDb().'\'';
  3797. $r_clean = $this->db->query($q_clean, true);
  3798. $q = 'SELECT count(*) AS c FROM inbound_email_autoreply WHERE deleted = 0 AND autoreplied_to = \''.$from.'\' AND ie_id = \''.$this->id.'\'';
  3799. $r = $this->db->query($q, true);
  3800. $a = $this->db->fetchByAssoc($r);
  3801. $email_num_autoreplies_24_hours = $this->get_stored_options('email_num_autoreplies_24_hours');
  3802. $maxReplies = (isset($email_num_autoreplies_24_hours)) ? $email_num_autoreplies_24_hours : $this->maxEmailNumAutoreplies24Hours;
  3803. if($a['c'] >= $maxReplies) {
  3804. $GLOBALS['log']->debug('Autoreply cancelled - more than ' . $maxReplies . ' replies sent in 24 hours.');
  3805. return false;
  3806. } else {
  3807. return true;
  3808. }
  3809. }
  3810. /**
  3811. * returns exactly 1 id match. if more than one, than returns false
  3812. * @param $emailName the subject of the email to match
  3813. * @param $tableName the table of the matching bean type
  3814. */
  3815. function getSingularRelatedId($emailName, $tableName) {
  3816. $repStrings = array('RE:','Re:','re:');
  3817. $preppedName = str_replace($repStrings,'',trim($emailName));
  3818. //TODO add team security to this query
  3819. $q = 'SELECT count(id) AS c FROM '.$tableName.' WHERE deleted = 0 AND name LIKE \'%'.$preppedName.'%\'';
  3820. $r = $this->db->query($q, true);
  3821. $a = $this->db->fetchByAssoc($r);
  3822. if($a['c'] == 0) {
  3823. $q = 'SELECT id FROM '.$tableName.' WHERE deleted = 0 AND name LIKE \'%'.$preppedName.'%\'';
  3824. $r = $this->db->query($q, true);
  3825. $a = $this->db->fetchByAssoc($r);
  3826. return $a['id'];
  3827. } else {
  3828. return false;
  3829. }
  3830. }
  3831. /**
  3832. * saves InboundEmail parse macros to config.php
  3833. * @param string type Bean to link
  3834. * @param string macro The new macro
  3835. */
  3836. function saveInboundEmailSystemSettings($type, $macro) {
  3837. global $sugar_config;
  3838. // inbound_email_case_subject_macro
  3839. $var = "inbound_email_".strtolower($type)."_subject_macro";
  3840. $sugar_config[$var] = $macro;
  3841. ksort($sugar_config);
  3842. $sugar_config_string = "<?php\n" .
  3843. '// created: ' . date('Y-m-d H:i:s') . "\n" .
  3844. '$sugar_config = ' .
  3845. var_export($sugar_config, true) .
  3846. ";\n?>\n";
  3847. write_array_to_file("sugar_config", $sugar_config, "config.php");
  3848. }
  3849. /**
  3850. * returns the HTML for InboundEmail system settings
  3851. * @return string HTML
  3852. */
  3853. function getSystemSettingsForm() {
  3854. global $sugar_config;
  3855. global $mod_strings;
  3856. global $app_strings;
  3857. global $app_list_strings;
  3858. //// Case Macro
  3859. $c = new aCase();
  3860. $macro = $c->getEmailSubjectMacro();
  3861. $ret =<<<eoq
  3862. <form action="index.php" method="post" name="Macro" id="form">
  3863. <input type="hidden" name="module" value="InboundEmail">
  3864. <input type="hidden" name="action" value="ListView">
  3865. <input type="hidden" name="save" value="true">
  3866. <table width="100%" cellpadding="0" cellspacing="0" border="0">
  3867. <tr>
  3868. <td>
  3869. <input title="{$app_strings['LBL_SAVE_BUTTON_TITLE']}"
  3870. accessKey="{$app_strings['LBL_SAVE_BUTTON_KEY']}"
  3871. class="button"
  3872. onclick="this.form.return_module.value='InboundEmail'; this.form.return_action.value='ListView';"
  3873. type="submit" name="Edit" value=" {$app_strings['LBL_SAVE_BUTTON_LABEL']} ">
  3874. </td>
  3875. </tr>
  3876. </table>
  3877. <table width="100%" border="0" cellspacing="0" cellpadding="0" class="detail view">
  3878. <tr>
  3879. <td valign="top" width='10%' NOWRAP scope="row">
  3880. <slot>
  3881. <b>{$mod_strings['LBL_CASE_MACRO']}:</b>
  3882. </slot>
  3883. </td>
  3884. <td valign="top" width='20%'>
  3885. <slot>
  3886. <input name="inbound_email_case_macro" type="text" value="{$macro}">
  3887. </slot>
  3888. </td>
  3889. <td valign="top" width='70%'>
  3890. <slot>
  3891. {$mod_strings['LBL_CASE_MACRO_DESC']}
  3892. <br />
  3893. <i>{$mod_strings['LBL_CASE_MACRO_DESC2']}</i>
  3894. </slot>
  3895. </td>
  3896. </tr>
  3897. </table>
  3898. </form>
  3899. eoq;
  3900. return $ret;
  3901. }
  3902. /**
  3903. * for mailboxes of type "Support" parse for '[CASE:%1]'
  3904. * @param $emailName the subject line of the email
  3905. * @param $aCase a Case object
  3906. */
  3907. function getCaseIdFromCaseNumber($emailName, $aCase) {
  3908. //$emailSubjectMacro
  3909. $exMacro = explode('%1', $aCase->getEmailSubjectMacro());
  3910. $open = $exMacro[0];
  3911. $close = $exMacro[1];
  3912. if($sub = stristr($emailName, $open)) { // eliminate everything up to the beginning of the macro and return the rest
  3913. // $sub is [CASE:XX] xxxxxxxxxxxxxxxxxxxxxx
  3914. $sub2 = str_replace($open, '', $sub);
  3915. // $sub2 is XX] xxxxxxxxxxxxxx
  3916. $sub3 = substr($sub2, 0, strpos($sub2, $close));
  3917. // filter out deleted records in order to create a new case
  3918. // if email is related to deleted one (bug #49840)
  3919. $r = $this->db->query("SELECT id FROM cases WHERE case_number = '{$sub3}' and deleted = 0", true);
  3920. $a = $this->db->fetchByAssoc($r);
  3921. if(!empty($a['id'])) {
  3922. return $a['id'];
  3923. } else {
  3924. return false;
  3925. }
  3926. } else {
  3927. return false;
  3928. }
  3929. }
  3930. function get_stored_options($option_name,$default_value=null,$stored_options=null) {
  3931. if (empty($stored_options)) {
  3932. $stored_options=$this->stored_options;
  3933. }
  3934. if(!empty($stored_options)) {
  3935. $storedOptions = unserialize(base64_decode($stored_options));
  3936. if (isset($storedOptions[$option_name])) {
  3937. $default_value=$storedOptions[$option_name];
  3938. }
  3939. }
  3940. return $default_value;
  3941. }
  3942. /**
  3943. * This function returns a contact or user ID if a matching email is found
  3944. * @param $email the email address to match
  3945. * @param $table which table to query
  3946. */
  3947. function getRelatedId($email, $module) {
  3948. $email = trim(strtoupper($email));
  3949. if(strpos($email, ',') !== false) {
  3950. $emailsArray = explode(',', $email);
  3951. $emailAddressString = "";
  3952. foreach($emailsArray as $emailAddress) {
  3953. if (!empty($emailAddressString)) {
  3954. $emailAddressString .= ",";
  3955. }
  3956. $emailAddressString .= "'" . $emailAddress. "'";
  3957. } // foreach
  3958. $email = $emailAddressString;
  3959. } else {
  3960. $email = "'" . $email . "'";
  3961. } // else
  3962. $module = ucfirst($module);
  3963. $q = "SELECT bean_id FROM email_addr_bean_rel eabr
  3964. JOIN email_addresses ea ON (eabr.email_address_id = ea.id)
  3965. WHERE bean_module = '{$module}' AND ea.email_address_caps in ( {$email} ) AND eabr.deleted=0";
  3966. $r = $this->db->query($q, true);
  3967. $retArr = array();
  3968. while($a = $this->db->fetchByAssoc($r)) {
  3969. $retArr[] = $a['bean_id'];
  3970. }
  3971. if(count($retArr) > 0) {
  3972. return $retArr;
  3973. } else {
  3974. return false;
  3975. }
  3976. }
  3977. /**
  3978. * finds emails tagged "//UNSEEN" on mailserver and "SINCE: [date]" if that
  3979. * option is set
  3980. *
  3981. * @return array Array of messageNumbers (mail server's internal keys)
  3982. */
  3983. function getNewMessageIds() {
  3984. $storedOptions = unserialize(base64_decode($this->stored_options));
  3985. //TODO figure out if the since date is UDT
  3986. if($storedOptions['only_since']) {// POP3 does not support Unseen flags
  3987. if(!isset($storedOptions['only_since_last']) && !empty($storedOptions['only_since_last'])) {
  3988. $q = 'SELECT last_run FROM schedulers WHERE job = \'function::pollMonitoredInboxes\'';
  3989. $r = $this->db->query($q, true);
  3990. $a = $this->db->fetchByAssoc($r);
  3991. $date = date('r', strtotime($a['last_run']));
  3992. } else {
  3993. $date = $storedOptions['only_since_last'];
  3994. }
  3995. $ret = imap_search($this->conn, 'SINCE "'.$date.'" UNDELETED UNSEEN');
  3996. $check = imap_check($this->conn);
  3997. $storedOptions['only_since_last'] = $check->Date;
  3998. $this->stored_options = base64_encode(serialize($storedOptions));
  3999. $this->save();
  4000. } else {
  4001. $ret = imap_search($this->conn, 'UNDELETED UNSEEN');
  4002. }
  4003. $GLOBALS['log']->debug('-----> getNewMessageIds() got '.count($ret).' new Messages');
  4004. return $ret;
  4005. }
  4006. /**
  4007. * Constructs the resource connection string that IMAP needs
  4008. * @param string $service Service string, will generate if not passed
  4009. * @return string
  4010. */
  4011. function getConnectString($service='', $mbox='', $includeMbox=true) {
  4012. $service = empty($service) ? $this->getServiceString() : $service;
  4013. $mbox = empty($mbox) ? $this->mailbox : $mbox;
  4014. $connectString = '{'.$this->server_url.':'.$this->port.'/service='.$this->protocol.$service.'}';
  4015. $connectString .= ($includeMbox) ? $mbox : "";
  4016. return $connectString;
  4017. }
  4018. function disconnectMailserver() {
  4019. if(is_resource($this->conn)) {
  4020. imap_close($this->conn);
  4021. }
  4022. }
  4023. /**
  4024. * Connects to mailserver. If an existing IMAP resource is available, it
  4025. * will attempt to reuse the connection, updating the mailbox path.
  4026. *
  4027. * @param bool test Flag to test connection
  4028. * @param bool force Force reconnect
  4029. * @return string "true" on success, "false" or $errorMessage on failure
  4030. */
  4031. function connectMailserver($test=false, $force=false) {
  4032. global $mod_strings;
  4033. if(!function_exists("imap_open")) {
  4034. $GLOBALS['log']->debug('------------------------- IMAP libraries NOT available!!!! die()ing thread.----');
  4035. return $mod_strings['LBL_WARN_NO_IMAP'];
  4036. }
  4037. imap_errors(); // clearing error stack
  4038. error_reporting(0); // turn off notices from IMAP
  4039. // tls::ca::ssl::protocol::novalidate-cert::notls
  4040. $useSsl = ($_REQUEST['ssl'] == 'true') ? true : false;
  4041. if($test) {
  4042. imap_timeout(1, 15); // 60 secs is the default
  4043. imap_timeout(2, 15);
  4044. imap_timeout(3, 15);
  4045. $opts = $this->findOptimumSettings($useSsl);
  4046. if(isset($opts['good']) && empty($opts['good'])) {
  4047. return array_pop($opts['err']);
  4048. } else {
  4049. $service = $opts['service'];
  4050. $service = str_replace('foo','', $service); // foo there to support no-item explodes
  4051. }
  4052. } else {
  4053. $service = $this->getServiceString();
  4054. }
  4055. $connectString = $this->getConnectString($service, $this->mailbox);
  4056. /*
  4057. * Try to recycle the current connection to reduce response times
  4058. */
  4059. if(is_resource($this->conn)) {
  4060. if($force) {
  4061. // force disconnect
  4062. imap_close($this->conn);
  4063. }
  4064. if(imap_ping($this->conn)) {
  4065. // we have a live connection
  4066. imap_reopen($this->conn, $connectString, CL_EXPUNGE);
  4067. }
  4068. }
  4069. // final test
  4070. if(!is_resource($this->conn) && !$test) {
  4071. $this->conn = imap_open($connectString, $this->email_user, $this->email_password, CL_EXPUNGE);
  4072. }
  4073. if($test) {
  4074. if ($opts == false && !is_resource($this->conn)) {
  4075. $this->conn = imap_open($connectString, $this->email_user, $this->email_password, CL_EXPUNGE);
  4076. }
  4077. $errors = '';
  4078. $alerts = '';
  4079. $successful = false;
  4080. if(($errors = imap_last_error()) || ($alerts = imap_alerts())) {
  4081. if($errors == 'Mailbox is empty') { // false positive
  4082. $successful = true;
  4083. } else {
  4084. $msg .= $errors;
  4085. $msg .= '<p>'.$alerts.'<p>';
  4086. $msg .= '<p>'.$mod_strings['ERR_TEST_MAILBOX'];
  4087. }
  4088. } else {
  4089. $successful = true;
  4090. }
  4091. if($successful) {
  4092. if($this->protocol == 'imap') {
  4093. $msg .= $mod_strings['LBL_TEST_SUCCESSFUL'];
  4094. /*
  4095. $testConnectString = '{'.$this->server_url.':'.$this->port.'/service='.$this->protocol.$service.'}';
  4096. if (!is_resource($this->conn)) {
  4097. $this->conn = imap_open($connectString, $this->email_user, $this->email_password, CL_EXPUNGE);
  4098. }
  4099. $list = imap_getmailboxes($this->conn, $testConnectString, "*");
  4100. if(isset($_REQUEST['personal']) && $_REQUEST['personal'] == 'true') {
  4101. $msg .= $mod_strings['LBL_TEST_SUCCESSFUL'];
  4102. } elseif (is_array($list)) {
  4103. sort($list);
  4104. _ppd($boxes);
  4105. $msg .= '<b>'.$mod_strings['LBL_FOUND_MAILBOXES'].'</b><p>';
  4106. foreach ($list as $key => $val) {
  4107. $mb = imap_utf7_decode(str_replace($testConnectString,'',$val->name));
  4108. $msg .= '<a onClick=\'setMailbox(\"'.$mb.'\"); window.close();\'>';
  4109. $msg .= $mb;
  4110. $msg .= '</a><br>';
  4111. }
  4112. } else {
  4113. $msg .= $errors;
  4114. $msg .= '<p>'.$mod_strings['ERR_MAILBOX_FAIL'].imap_last_error().'</p>';
  4115. $msg .= '<p>'.$mod_strings['ERR_TEST_MAILBOX'].'</p>';
  4116. }
  4117. */
  4118. } else {
  4119. $msg .= $mod_strings['LBL_POP3_SUCCESS'];
  4120. }
  4121. }
  4122. imap_errors(); // collapse error stack
  4123. imap_close($this->conn);
  4124. return $msg;
  4125. } elseif(!is_resource($this->conn)) {
  4126. return "false";
  4127. } else {
  4128. return "true";
  4129. }
  4130. }
  4131. function checkImap() {
  4132. global $mod_strings;
  4133. if(!function_exists('imap_open')) {
  4134. echo '
  4135. <table cellpadding="0" cellspacing="0" width="100%" border="0" class="list view">
  4136. <tr height="20">
  4137. <td scope="col" width="25%" colspan="2"><slot>
  4138. '.$mod_strings['LBL_WARN_IMAP_TITLE'].'
  4139. </slot></td>
  4140. </tr>
  4141. <tr>
  4142. <td scope="row" valign=TOP bgcolor="#fdfdfd" width="20%"><slot>
  4143. '.$mod_strings['LBL_WARN_IMAP'].'
  4144. <td scope="row" valign=TOP class="oddListRowS1" bgcolor="#fdfdfd" width="80%"><slot>
  4145. <span class=error>'.$mod_strings['LBL_WARN_NO_IMAP'].'</span>
  4146. </slot></td>
  4147. </tr>
  4148. </table>
  4149. <br>';
  4150. }
  4151. }
  4152. /**
  4153. * retrieves an array of I-E beans based on the group_id
  4154. * @param string $groupId GUID of the group user or Individual
  4155. * @return array $beans array of beans
  4156. * @return boolean false if none returned
  4157. */
  4158. function retrieveByGroupId($groupId) {
  4159. $q = 'SELECT id FROM inbound_email WHERE group_id = \''.$groupId.'\' AND deleted = 0 AND status = \'Active\'';
  4160. $r = $this->db->query($q, true);
  4161. $beans = array();
  4162. while($a = $this->db->fetchByAssoc($r)) {
  4163. $ie = new InboundEmail();
  4164. $ie->retrieve($a['id']);
  4165. $beans[$a['id']] = $ie;
  4166. }
  4167. return $beans;
  4168. }
  4169. /**
  4170. * Retrieves the current count of personal accounts for the user specified.
  4171. *
  4172. * @param unknown_type $user
  4173. */
  4174. function getUserPersonalAccountCount($user = null)
  4175. {
  4176. if($user == null)
  4177. $user = $GLOBALS['current_user'];
  4178. $query = "SELECT count(*) as c FROM inbound_email WHERE deleted=0 AND is_personal='1' AND group_id='{$user->id}' AND status='Active'";
  4179. $rs = $this->db->query($query);
  4180. $row = $this->db->fetchByAssoc($rs);
  4181. return $row['c'];
  4182. }
  4183. /**
  4184. * retrieves an array of I-E beans based on the group folder id
  4185. * @param string $groupFolderId GUID of the group folder
  4186. * @return array $beans array of beans
  4187. * @return boolean false if none returned
  4188. */
  4189. function retrieveByGroupFolderId($groupFolderId) {
  4190. $q = 'SELECT id FROM inbound_email WHERE groupfolder_id = \''.$groupFolderId.'\' AND deleted = 0 ';
  4191. $r = $this->db->query($q, true);
  4192. $beans = array();
  4193. while($a = $this->db->fetchByAssoc($r)) {
  4194. $ie = new InboundEmail();
  4195. $ie->retrieve($a['id']);
  4196. $beans[] = $ie;
  4197. }
  4198. return $beans;
  4199. }
  4200. /**
  4201. * Retrieves an array of I-E beans that the user has team access to
  4202. */
  4203. function retrieveAllByGroupId($id, $includePersonal=true) {
  4204. global $current_user;
  4205. $beans = ($includePersonal) ? $this->retrieveByGroupId($id) : array();
  4206. $teamJoin = '';
  4207. // bug 50536: groupfolder_id cannot be updated to NULL from sugarbean's nullable check ('type' set to ID in the vardef)
  4208. // hence the awkward or check -- rbacon
  4209. $q = "SELECT inbound_email.id FROM inbound_email {$teamJoin} WHERE is_personal = 0 AND (groupfolder_id is null OR groupfolder_id = '') AND mailbox_type not like 'bounce' AND inbound_email.deleted = 0 AND status = 'Active' ";
  4210. $r = $this->db->query($q, true);
  4211. while($a = $this->db->fetchByAssoc($r)) {
  4212. $found = false;
  4213. foreach($beans as $bean) {
  4214. if($bean->id == $a['id']) {
  4215. $found = true;
  4216. }
  4217. }
  4218. if(!$found) {
  4219. $ie = new InboundEmail();
  4220. $ie->retrieve($a['id']);
  4221. $beans[$a['id']] = $ie;
  4222. }
  4223. }
  4224. return $beans;
  4225. }
  4226. /**
  4227. * Retrieves an array of I-E beans that the user has team access to including group
  4228. */
  4229. function retrieveAllByGroupIdWithGroupAccounts($id, $includePersonal=true) {
  4230. global $current_user;
  4231. $beans = ($includePersonal) ? $this->retrieveByGroupId($id) : array();
  4232. $teamJoin = '';
  4233. $q = "SELECT DISTINCT inbound_email.id FROM inbound_email {$teamJoin} WHERE is_personal = 0 AND mailbox_type not like 'bounce' AND status = 'Active' AND inbound_email.deleted = 0 ";
  4234. $r = $this->db->query($q, true);
  4235. while($a = $this->db->fetchByAssoc($r)) {
  4236. $found = false;
  4237. foreach($beans as $bean) {
  4238. if($bean->id == $a['id']) {
  4239. $found = true;
  4240. }
  4241. }
  4242. if(!$found) {
  4243. $ie = new InboundEmail();
  4244. $ie->retrieve($a['id']);
  4245. $beans[$a['id']] = $ie;
  4246. }
  4247. }
  4248. return $beans;
  4249. }
  4250. /**
  4251. * returns the bean name - overrides SugarBean's
  4252. */
  4253. function get_summary_text() {
  4254. return $this->name;
  4255. }
  4256. /**
  4257. * Override's SugarBean's
  4258. */
  4259. function create_export_query($order_by, $where, $show_deleted = 0) {
  4260. return $this->create_new_list_query($order_by, $where, $show_deleted = 0);
  4261. }
  4262. /**
  4263. * Override's SugarBean's
  4264. */
  4265. /**
  4266. * Override's SugarBean's
  4267. */
  4268. function get_list_view_data(){
  4269. global $mod_strings;
  4270. global $app_list_strings;
  4271. $temp_array = $this->get_list_view_array();
  4272. $temp_array['MAILBOX_TYPE_NAME']= $app_list_strings['dom_mailbox_type'][$this->mailbox_type];
  4273. //cma, fix bug 21670.
  4274. $temp_array['GLOBAL_PERSONAL_STRING']= ($this->is_personal ? $mod_strings['LBL_IS_PERSONAL'] : $mod_strings['LBL_IS_GROUP']);
  4275. $temp_array['STATUS'] = ($this->status == 'Active') ? $mod_strings['LBL_STATUS_ACTIVE'] : $mod_strings['LBL_STATUS_INACTIVE'];
  4276. return $temp_array;
  4277. }
  4278. /**
  4279. * Override's SugarBean's
  4280. */
  4281. function fill_in_additional_list_fields() {
  4282. $this->fill_in_additional_detail_fields();
  4283. }
  4284. /**
  4285. * Override's SugarBean's
  4286. */
  4287. function fill_in_additional_detail_fields() {
  4288. if(!empty($this->service)) {
  4289. $exServ = explode('::', $this->service);
  4290. $this->tls = $exServ[0];
  4291. if ( isset($exServ[1]) )
  4292. $this->ca = $exServ[1];
  4293. if ( isset($exServ[2]) )
  4294. $this->ssl = $exServ[2];
  4295. if ( isset($exServ[3]) )
  4296. $this->protocol = $exServ[3];
  4297. }
  4298. }
  4299. ///////////////////////////////////////////////////////////////////////////
  4300. //// IN SUPPORT OF EMAIL 2.0
  4301. /**
  4302. * Checks for $user's autoImport setting and returns the current value
  4303. * @param object $user User in focus, defaults to $current_user
  4304. * @return bool
  4305. */
  4306. function isAutoImport($user=null) {
  4307. if(!empty($this->autoImport)) {
  4308. return $this->autoImport;
  4309. }
  4310. global $current_user;
  4311. if(empty($user)) $user = $current_user;
  4312. $emailSettings = $current_user->getPreference('emailSettings', 'Emails');
  4313. $emailSettings = is_string($emailSettings) ? unserialize($emailSettings) : $emailSettings;
  4314. $this->autoImport = (isset($emailSettings['autoImport']) && !empty($emailSettings['autoImport'])) ? true : false;
  4315. return $this->autoImport;
  4316. }
  4317. /**
  4318. * Clears out cache files for a user
  4319. */
  4320. function cleanOutCache() {
  4321. $GLOBALS['log']->debug("INBOUNDEMAIL: at cleanOutCache()");
  4322. $this->deleteCache();
  4323. }
  4324. /**
  4325. * moves emails from folder to folder
  4326. * @param string $fromIe I-E id
  4327. * @param string $fromFolder IMAP path to folder in which the email lives
  4328. * @param string $toIe I-E id
  4329. * @param string $toFolder
  4330. * @param string $uids UIDs of emails to move, either Sugar GUIDS or IMAP
  4331. * UIDs
  4332. */
  4333. function copyEmails($fromIe, $fromFolder, $toIe, $toFolder, $uids) {
  4334. $this->moveEmails($fromIe, $fromFolder, $toIe, $toFolder, $uids, true);
  4335. }
  4336. /**
  4337. * moves emails from folder to folder
  4338. * @param string $fromIe I-E id
  4339. * @param string $fromFolder IMAP path to folder in which the email lives
  4340. * @param string $toIe I-E id
  4341. * @param string $toFolder
  4342. * @param string $uids UIDs of emails to move, either Sugar GUIDS or IMAP
  4343. * UIDs
  4344. * @param bool $copy Default false
  4345. * @return bool True on successful execution
  4346. */
  4347. function moveEmails($fromIe, $fromFolder, $toIe, $toFolder, $uids, $copy=false) {
  4348. global $app_strings;
  4349. global $current_user;
  4350. // same I-E server
  4351. if($fromIe == $toIe) {
  4352. $GLOBALS['log']->debug("********* SUGARFOLDER - moveEmails() moving email from I-E to I-E");
  4353. //$exDestFolder = explode("::", $toFolder);
  4354. //preserve $this->mailbox
  4355. if (isset($this->mailbox)) {
  4356. $oldMailbox = $this->mailbox;
  4357. }
  4358. $this->retrieve($fromIe);
  4359. $this->mailbox = $fromFolder;
  4360. $this->connectMailserver();
  4361. $exUids = explode('::;::', $uids);
  4362. $uids = implode(",", $exUids);
  4363. // imap_mail_move accepts comma-delimited lists of UIDs
  4364. if($copy) {
  4365. if(imap_mail_copy($this->conn, $uids, $toFolder, CP_UID)) {
  4366. $this->mailbox = $toFolder;
  4367. $this->connectMailserver();
  4368. $newOverviews = imap_fetch_overview($this->conn, $uids, FT_UID);
  4369. $this->updateOverviewCacheFile($newOverviews, 'append');
  4370. if (isset($oldMailbox)) {
  4371. $this->mailbox = $oldMailbox;
  4372. }
  4373. return true;
  4374. } else {
  4375. $GLOBALS['log']->debug("INBOUNDEMAIL: could not imap_mail_copy() [ {$uids} ] to folder [ {$toFolder} ] from folder [ {$fromFolder} ]");
  4376. }
  4377. } else {
  4378. if(imap_mail_move($this->conn, $uids, $toFolder, CP_UID)) {
  4379. $GLOBALS['log']->info("INBOUNDEMAIL: imap_mail_move() [ {$uids} ] to folder [ {$toFolder} ] from folder [ {$fromFolder} ]");
  4380. imap_expunge($this->conn); // hard deletes moved messages
  4381. // update cache on fromFolder
  4382. $newOverviews = $this->getOverviewsFromCacheFile($uids, $fromFolder, true);
  4383. $this->deleteCachedMessages($uids, $fromFolder);
  4384. // update cache on toFolder
  4385. $this->checkEmailOneMailbox($toFolder, true, true);
  4386. if (isset($oldMailbox)) {
  4387. $this->mailbox = $oldMailbox;
  4388. }
  4389. return true;
  4390. } else {
  4391. $GLOBALS['log']->debug("INBOUNDEMAIL: could not imap_mail_move() [ {$uids} ] to folder [ {$toFolder} ] from folder [ {$fromFolder} ]");
  4392. }
  4393. }
  4394. } elseif($toIe == 'folder' && $fromFolder == 'sugar::Emails') {
  4395. $GLOBALS['log']->debug("********* SUGARFOLDER - moveEmails() moving email from SugarFolder to SugarFolder");
  4396. // move from sugar folder to sugar folder
  4397. require_once("include/SugarFolders/SugarFolders.php");
  4398. $sugarFolder = new SugarFolder();
  4399. $exUids = explode($app_strings['LBL_EMAIL_DELIMITER'], $uids);
  4400. foreach($exUids as $id) {
  4401. if($copy) {
  4402. $sugarFolder->copyBean($fromIe, $toFolder, $id, "Emails");
  4403. } else {
  4404. $fromSugarFolder = new SugarFolder();
  4405. $fromSugarFolder->retrieve($fromIe);
  4406. $toSugarFolder = new SugarFolder();
  4407. $toSugarFolder->retrieve($toFolder);
  4408. $email = new Email();
  4409. $email->retrieve($id);
  4410. $email->status = 'unread';
  4411. // when you move from My Emails to Group Folder, Assign To field for the Email should become null
  4412. if ($fromSugarFolder->is_dynamic && $toSugarFolder->is_group) {
  4413. // Bug 50972 - assigned_user_id set to empty string not true null
  4414. // Modifying the field defs in just this one place to allow
  4415. // a true null since this is what is expected when reading
  4416. // inbox folders
  4417. $email->setFieldNullable('assigned_user_id');
  4418. $email->assigned_user_id = "";
  4419. $email->save();
  4420. $email->revertFieldNullable('assigned_user_id');
  4421. // End fix 50972
  4422. if (!$toSugarFolder->checkEmailExistForFolder($id)) {
  4423. $fromSugarFolder->deleteEmailFromAllFolder($id);
  4424. $toSugarFolder->addBean($email);
  4425. }
  4426. } elseif ($fromSugarFolder->is_group && $toSugarFolder->is_dynamic) {
  4427. $fromSugarFolder->deleteEmailFromAllFolder($id);
  4428. $email->assigned_user_id = $current_user->id;
  4429. $email->save();
  4430. } else {
  4431. // If you are moving something from personal folder then delete an entry from all folder
  4432. if (!$fromSugarFolder->is_dynamic && !$fromSugarFolder->is_group) {
  4433. $fromSugarFolder->deleteEmailFromAllFolder($id);
  4434. } // if
  4435. if ($fromSugarFolder->is_dynamic && !$toSugarFolder->is_dynamic && !$toSugarFolder->is_group) {
  4436. $email->assigned_user_id = "";
  4437. $toSugarFolder->addBean($email);
  4438. } // if
  4439. if (!$toSugarFolder->checkEmailExistForFolder($id)) {
  4440. if (!$toSugarFolder->is_dynamic) {
  4441. $fromSugarFolder->deleteEmailFromAllFolder($id);
  4442. $toSugarFolder->addBean($email);
  4443. } else {
  4444. $fromSugarFolder->deleteEmailFromAllFolder($id);
  4445. $email->assigned_user_id = $current_user->id;
  4446. }
  4447. } else {
  4448. $sugarFolder->move($fromIe, $toFolder, $id);
  4449. } // else
  4450. $email->save();
  4451. } // else
  4452. }
  4453. }
  4454. return true;
  4455. } elseif($toIe == 'folder') {
  4456. $GLOBALS['log']->debug("********* SUGARFOLDER - moveEmails() moving email from I-E to SugarFolder");
  4457. // move to Sugar folder
  4458. require_once("include/SugarFolders/SugarFolders.php");
  4459. $sugarFolder = new SugarFolder();
  4460. $sugarFolder->retrieve($toFolder);
  4461. //Show the import form if we don't have the required info
  4462. if (!isset($_REQUEST['delete'])) {
  4463. $json = getJSONobj();
  4464. if ($sugarFolder->is_group) {
  4465. $_REQUEST['showTeam'] = false;
  4466. $_REQUEST['showAssignTo'] = false;
  4467. }
  4468. $ret = $this->email->et->getImportForm($_REQUEST, $this->email);
  4469. $ret['move'] = true;
  4470. $ret['srcFolder'] = $fromFolder;
  4471. $ret['srcIeId'] = $fromIe;
  4472. $ret['dstFolder'] = $toFolder;
  4473. $ret['dstIeId'] = $toIe;
  4474. $out = trim($json->encode($ret, false));
  4475. echo $out;
  4476. return true;
  4477. }
  4478. // import to Sugar
  4479. $this->retrieve($fromIe);
  4480. $this->mailbox = $fromFolder;
  4481. $this->connectMailserver();
  4482. // If its a group folder the team should be of the folder team
  4483. if ($sugarFolder->is_group) {
  4484. $_REQUEST['team_id'] = $sugarFolder->team_id;
  4485. $_REQUEST['team_set_id'] = $sugarFolder->team_set_id;
  4486. } else {
  4487. // TODO - set team_id, team_set for new UI
  4488. } // else
  4489. $exUids = explode($app_strings['LBL_EMAIL_DELIMITER'], $uids);
  4490. if(!empty($sugarFolder->id)) {
  4491. $count = 1;
  4492. $return = array();
  4493. $json = getJSONobj();
  4494. foreach($exUids as $k => $uid) {
  4495. $msgNo = $uid;
  4496. if ($this->isPop3Protocol()) {
  4497. $msgNo = $this->getCorrectMessageNoForPop3($uid);
  4498. } else {
  4499. $msgNo = imap_msgno($this->conn, $uid);
  4500. }
  4501. if(!empty($msgNo)) {
  4502. $importStatus = $this->importOneEmail($msgNo, $uid);
  4503. // add to folder
  4504. if($importStatus) {
  4505. $sugarFolder->addBean($this->email);
  4506. if(!$copy && isset($_REQUEST['delete']) && ($_REQUEST['delete'] == "true") && $importStatus) {
  4507. $GLOBALS['log']->error("********* delete from mailserver [ {explode(",", $uids)} ]");
  4508. // delete from mailserver
  4509. $this->deleteMessageOnMailServer($uid);
  4510. $this->deleteMessageFromCache($uid);
  4511. } // if
  4512. }
  4513. $return[] = $app_strings['LBL_EMAIL_MESSAGE_NO'] . " " . $count . ", " . $app_strings['LBL_STATUS'] . " " . ($importStatus ? $app_strings['LBL_EMAIL_IMPORT_SUCCESS'] : $app_strings['LBL_EMAIL_IMPORT_FAIL']);
  4514. $count++;
  4515. } // if
  4516. } // foreach
  4517. echo $json->encode($return);
  4518. return true;
  4519. } else {
  4520. $GLOBALS['log']->error("********* SUGARFOLDER - failed to retrieve folder ID [ {$toFolder} ]");
  4521. }
  4522. } else {
  4523. $GLOBALS['log']->debug("********* SUGARFOLDER - moveEmails() called with no passing criteria");
  4524. }
  4525. return false;
  4526. }
  4527. /**
  4528. * Hard deletes an I-E account
  4529. * @param string id GUID
  4530. */
  4531. function hardDelete($id) {
  4532. $q = "DELETE FROM inbound_email WHERE id = '{$id}'";
  4533. $r = $this->db->query($q, true);
  4534. }
  4535. /**
  4536. * Generate a unique filename for attachments based on the message id. There are no maximum
  4537. * specifications for the length of the message id, the only requirement is that it be globally unique.
  4538. *
  4539. * @param bool $nameOnly Whether or not the attachment count should be appended to the filename.
  4540. * @return string The temp file name
  4541. */
  4542. function getTempFilename($nameOnly=false) {
  4543. $str = $this->compoundMessageId;
  4544. if(!$nameOnly) {
  4545. $str = $str.$this->attachmentCount;
  4546. $this->attachmentCount++;
  4547. }
  4548. return $str;
  4549. }
  4550. /**
  4551. * deletes and expunges emails on server
  4552. * @param string $uid UID(s), comma delimited, of email(s) on server
  4553. * @return bool true on success
  4554. */
  4555. function deleteMessageOnMailServer($uid) {
  4556. global $app_strings;
  4557. $this->connectMailserver();
  4558. if(strpos($uid, $app_strings['LBL_EMAIL_DELIMITER']) !== false) {
  4559. $uids = explode($app_strings['LBL_EMAIL_DELIMITER'], $uid);
  4560. } else {
  4561. $uids[] = $uid;
  4562. }
  4563. $return = true;
  4564. if($this->protocol == 'imap') {
  4565. $trashFolder = $this->get_stored_options("trashFolder");
  4566. if (empty($trashFolder)) {
  4567. $trashFolder = "INBOX.Trash";
  4568. }
  4569. foreach($uids as $uid) {
  4570. if($this->moveEmails($this->id, $this->mailbox, $this->id, $trashFolder, $uid))
  4571. $GLOBALS['log']->debug("INBOUNDEMAIL: MoveEmail to {$trashFolder} successful.");
  4572. else {
  4573. $GLOBALS['log']->debug("INBOUNDEMAIL: MoveEmail to {$trashFolder} FAILED - trying hard delete for message: $uid");
  4574. imap_delete($this->conn, $uid, FT_UID);
  4575. $return = true;
  4576. }
  4577. }
  4578. }
  4579. else {
  4580. $msgnos = array();
  4581. foreach($uids as $uid) {
  4582. $msgnos[] = $this->getCorrectMessageNoForPop3($uid);
  4583. }
  4584. $msgnos = implode(',', $msgnos);
  4585. imap_delete($this->conn, $msgnos);
  4586. $return = true;
  4587. }
  4588. if(!imap_expunge($this->conn)) {
  4589. $GLOBALS['log']->debug("NOOP: could not expunge deleted email.");
  4590. $return = false;
  4591. }
  4592. else
  4593. $GLOBALS['log']->info("INBOUNDEMAIL: hard-deleted mail with MSgno's' [ {$msgnos} ]");
  4594. return $return;
  4595. }
  4596. /**
  4597. * deletes and expunges emails on server
  4598. * @param string $uid UID(s), comma delimited, of email(s) on server
  4599. */
  4600. function deleteMessageOnMailServerForPop3($uid) {
  4601. if(imap_delete($this->conn, $uid)) {
  4602. if(!imap_expunge($this->conn)) {
  4603. $GLOBALS['log']->debug("NOOP: could not expunge deleted email.");
  4604. $return = false;
  4605. } else {
  4606. $GLOBALS['log']->info("INBOUNDEMAIL: hard-deleted mail with MSgno's' [ {$uid} ]");
  4607. }
  4608. }
  4609. }
  4610. /**
  4611. * Checks if this is a pop3 type of an account or not
  4612. * @return boolean
  4613. */
  4614. function isPop3Protocol() {
  4615. return ($this->protocol == 'pop3');
  4616. }
  4617. /**
  4618. * Gets the UIDL from database for the corresponding msgno
  4619. * @param int messageNo of a message
  4620. * @return UIDL for the message
  4621. */
  4622. function getUIDLForMessage($msgNo) {
  4623. $query = "SELECT message_id FROM email_cache WHERE ie_id = '{$this->id}' AND msgno = '{$msgNo}'";
  4624. $r = $this->db->query($query);
  4625. $a = $this->db->fetchByAssoc($r);
  4626. return $a['message_id'];
  4627. }
  4628. /**
  4629. * Get the users default IE account id
  4630. *
  4631. * @param User $user
  4632. * @return string
  4633. */
  4634. function getUsersDefaultOutboundServerId($user)
  4635. {
  4636. $id = $user->getPreference($this->keyForUsersDefaultIEAccount,'Emails',$user);
  4637. //If no preference has been set, grab the default system id.
  4638. if(empty($id))
  4639. {
  4640. $oe = new OutboundEmail();
  4641. $system = $oe->getSystemMailerSettings();
  4642. $id=empty($system->id) ? '' : $system->id;
  4643. }
  4644. return $id;
  4645. }
  4646. /**
  4647. * Get the users default IE account id
  4648. *
  4649. * @param User $user
  4650. */
  4651. function setUsersDefaultOutboundServerId($user,$oe_id)
  4652. {
  4653. $user->setPreference($this->keyForUsersDefaultIEAccount, $oe_id, '', 'Emails');
  4654. }
  4655. /**
  4656. * Gets the UIDL from database for the corresponding msgno
  4657. * @param int messageNo of a message
  4658. * @return UIDL for the message
  4659. */
  4660. function getMsgnoForMessageID($messageid) {
  4661. $query = "SELECT msgno FROM email_cache WHERE ie_id = '{$this->id}' AND message_id = '{$messageid}'";
  4662. $r = $this->db->query($query);
  4663. $a = $this->db->fetchByAssoc($r);
  4664. return $a['message_id'];
  4665. }
  4666. /**
  4667. * fills InboundEmail->email with an email's details
  4668. * @param int uid Unique ID of email
  4669. * @param bool isMsgNo flag that passed ID is msgNo, default false
  4670. * @param bool setRead Sets the 'seen' flag in cache
  4671. * @param bool forceRefresh Skips cache file
  4672. * @return string
  4673. */
  4674. function setEmailForDisplay($uid, $isMsgNo=false, $setRead=false, $forceRefresh=false) {
  4675. if(empty($uid)) {
  4676. $GLOBALS['log']->debug("*** ERROR: INBOUNDEMAIL trying to setEmailForDisplay() with no UID");
  4677. return 'NOOP';
  4678. }
  4679. global $sugar_config;
  4680. global $app_strings;
  4681. // if its a pop3 then get the UIDL and see if this file name exist or not
  4682. if ($this->isPop3Protocol()) {
  4683. // get the UIDL from database;
  4684. $cachedUIDL = md5($uid);
  4685. $cache = "{$this->EmailCachePath}/{$this->id}/messages/{$this->mailbox}{$cachedUIDL}.php";
  4686. } else {
  4687. $cache = "{$this->EmailCachePath}/{$this->id}/messages/{$this->mailbox}{$uid}.php";
  4688. }
  4689. if(file_exists($cache) && !$forceRefresh) {
  4690. $GLOBALS['log']->info("INBOUNDEMAIL: Using cache file for setEmailForDisplay()");
  4691. include($cache); // profides $cacheFile
  4692. /** @var $cacheFile array */
  4693. $metaOut = unserialize($cacheFile['out']);
  4694. $meta = $metaOut['meta']['email'];
  4695. $email = new Email();
  4696. foreach($meta as $k => $v) {
  4697. $email->$k = $v;
  4698. }
  4699. $email->to_addrs = $meta['toaddrs'];
  4700. $email->date_sent = $meta['date_start'];
  4701. //_ppf($email,true);
  4702. $this->email = $email;
  4703. $this->email->email2init();
  4704. $ret = 'cache';
  4705. } else {
  4706. $GLOBALS['log']->info("INBOUNDEMAIL: opening new connection for setEmailForDisplay()");
  4707. if($this->isPop3Protocol()) {
  4708. $msgNo = $this->getCorrectMessageNoForPop3($uid);
  4709. } else {
  4710. if(empty($this->conn)) {
  4711. $this->connectMailserver();
  4712. }
  4713. $msgNo = ($isMsgNo) ? $uid : imap_msgno($this->conn, $uid);
  4714. }
  4715. if(empty($this->conn)) {
  4716. $status = $this->connectMailserver();
  4717. if($status == "false") {
  4718. $this->email = new Email();
  4719. $this->email->name = $app_strings['LBL_EMAIL_ERROR_MAILSERVERCONNECTION'];
  4720. $ret = 'error';
  4721. return $ret;
  4722. }
  4723. }
  4724. $this->importOneEmail($msgNo, $uid, true);
  4725. $this->email->id = '';
  4726. $this->email->new_with_id = false;
  4727. $ret = 'import';
  4728. }
  4729. if($setRead) {
  4730. $this->setStatuses($uid, 'seen', 1);
  4731. }
  4732. return $ret;
  4733. }
  4734. /**
  4735. * Sets status for a particular attribute on the mailserver and the local cache file
  4736. */
  4737. function setStatuses($uid, $field, $value) {
  4738. global $sugar_config;
  4739. /** available status fields
  4740. [subject] => aaa
  4741. [from] => Some Name
  4742. [to] => Some Name
  4743. [date] => Mon, 22 Jan 2007 17:32:57 -0800
  4744. [message_id] =>
  4745. [size] => 718
  4746. [uid] => 191
  4747. [msgno] => 141
  4748. [recent] => 0
  4749. [flagged] => 0
  4750. [answered] => 0
  4751. [deleted] => 0
  4752. [seen] => 1
  4753. [draft] => 0
  4754. */
  4755. // local cache
  4756. $file = "{$this->mailbox}.imapFetchOverview.php";
  4757. $overviews = $this->getCacheValueForUIDs($this->mailbox, array($uid));
  4758. if(!empty($overviews)) {
  4759. $updates = array();
  4760. foreach($overviews['retArr'] as $k => $obj) {
  4761. if($obj->imap_uid == $uid) {
  4762. $obj->$field = $value;
  4763. $updates[] = $obj;
  4764. }
  4765. }
  4766. if(!empty($updates)) {
  4767. $this->setCacheValue($this->mailbox, array(), $updates);
  4768. }
  4769. }
  4770. }
  4771. /**
  4772. * Removes an email from the cache file, deletes the message from the cache too
  4773. * @param string String of uids, comma delimited
  4774. */
  4775. function deleteMessageFromCache($uids) {
  4776. global $sugar_config;
  4777. global $app_strings;
  4778. // delete message cache file and email_cache file
  4779. $exUids = explode($app_strings['LBL_EMAIL_DELIMITER'], $uids);
  4780. foreach($exUids as $uid) {
  4781. // local cache
  4782. if ($this->isPop3Protocol()) {
  4783. $q = "DELETE FROM email_cache WHERE message_id = '{$uid}' AND ie_id = '{$this->id}'";
  4784. } else {
  4785. $q = "DELETE FROM email_cache WHERE imap_uid = {$uid} AND ie_id = '{$this->id}'";
  4786. }
  4787. $r = $this->db->query($q);
  4788. if ($this->isPop3Protocol()) {
  4789. $uid = md5($uid);
  4790. } // if
  4791. $msgCacheFile = "{$this->EmailCachePath}/{$this->id}/messages/{$this->mailbox}{$uid}.php";
  4792. if(file_exists($msgCacheFile)) {
  4793. if(!unlink($msgCacheFile)) {
  4794. $GLOBALS['log']->error("***ERROR: InboundEmail could not delete the cache file [ {$msgCacheFile} ]");
  4795. }
  4796. }
  4797. }
  4798. }
  4799. /**
  4800. * Shows one email.
  4801. * @param int uid UID of email to display
  4802. * @param string mbox Mailbox to look in for the message
  4803. * @param bool isMsgNo Flag to assume $uid is a MessageNo, not UniqueID, default false
  4804. */
  4805. function displayOneEmail($uid, $mbox, $isMsgNo=false) {
  4806. require_once("include/JSON.php");
  4807. global $timedate;
  4808. global $app_strings;
  4809. global $app_list_strings;
  4810. global $sugar_smarty;
  4811. global $theme;
  4812. global $current_user;
  4813. global $sugar_config;
  4814. $fetchedAttributes = array(
  4815. 'name',
  4816. 'from_name',
  4817. 'from_addr',
  4818. 'date_start',
  4819. 'time_start',
  4820. 'message_id',
  4821. );
  4822. $souEmail = array();
  4823. foreach($fetchedAttributes as $k) {
  4824. if ($k == 'date_start') {
  4825. $this->email->$k . " " . $this->email->time_start;
  4826. $souEmail[$k] = $this->email->$k . " " . $this->email->time_start;
  4827. } elseif ($k == 'time_start') {
  4828. $souEmail[$k] = "";
  4829. } else {
  4830. $souEmail[$k] = trim($this->email->$k);
  4831. }
  4832. }
  4833. // if a MsgNo is passed in, convert to UID
  4834. if($isMsgNo)
  4835. $uid = imap_uid($this->conn, $uid);
  4836. // meta object to allow quick retrieval for replies
  4837. $meta = array();
  4838. $meta['type'] = $this->email->type;
  4839. $meta['uid'] = $uid;
  4840. $meta['ieId'] = $this->id;
  4841. $meta['email'] = $souEmail;
  4842. $meta['mbox'] = $this->mailbox;
  4843. $ccs = '';
  4844. // imap vs pop3
  4845. // self mapping
  4846. $exMbox = explode("::", $mbox);
  4847. // CC section
  4848. $cc = '';
  4849. if(!empty($this->email->cc_addrs)) {
  4850. //$ccs = $this->collapseLongMailingList($this->email->cc_addrs);
  4851. $ccs = to_html($this->email->cc_addrs_names);
  4852. $cc =<<<eoq
  4853. <tr>
  4854. <td NOWRAP valign="top" class="displayEmailLabel">
  4855. {$app_strings['LBL_EMAIL_CC']}:
  4856. </td>
  4857. <td class="displayEmailValue">
  4858. {$ccs}
  4859. </td>
  4860. </tr>
  4861. eoq;
  4862. }
  4863. $meta['cc'] = $cc;
  4864. $meta['email']['cc_addrs'] = $ccs;
  4865. // attachments
  4866. $attachments = '';
  4867. if ($mbox == "sugar::Emails") {
  4868. $q = "SELECT id, filename, file_mime_type FROM notes WHERE parent_id = '{$uid}' AND deleted = 0";
  4869. $r = $this->db->query($q);
  4870. $i = 0;
  4871. while($a = $this->db->fetchByAssoc($r)) {
  4872. $url = "index.php?entryPoint=download&type=notes&id={$a['id']}";
  4873. $lbl = ($i == 0) ? $app_strings['LBL_EMAIL_ATTACHMENTS'].":" : '';
  4874. $i++;
  4875. $attachments .=<<<EOQ
  4876. <tr>
  4877. <td NOWRAP valign="top" class="displayEmailLabel">
  4878. {$lbl}
  4879. </td>
  4880. <td NOWRAP valign="top" colspan="2" class="displayEmailValue">
  4881. <a href="{$url}">{$a['filename']}</a>
  4882. </td>
  4883. </tr>
  4884. EOQ;
  4885. $this->email->cid2Link($a['id'], $a['file_mime_type']);
  4886. } // while
  4887. } else {
  4888. if($this->attachmentCount > 0) {
  4889. $theCount = $this->attachmentCount;
  4890. for($i=0; $i<$theCount; $i++) {
  4891. $lbl = ($i == 0) ? $app_strings['LBL_EMAIL_ATTACHMENTS'].":" : '';
  4892. $name = $this->getTempFilename(true).$i;
  4893. $tempName = urlencode($this->tempAttachment[$name]);
  4894. $url = "index.php?entryPoint=download&type=temp&isTempFile=true&ieId={$this->id}&tempName={$tempName}&id={$name}";
  4895. $attachments .=<<<eoq
  4896. <tr>
  4897. <td NOWRAP valign="top" class="displayEmailLabel">
  4898. {$lbl}
  4899. </td>
  4900. <td NOWRAP valign="top" colspan="2" class="displayEmailValue">
  4901. <a href="{$url}">{$this->tempAttachment[$name]}</a>
  4902. </td>
  4903. </tr>
  4904. eoq;
  4905. } // for
  4906. } // if
  4907. } // else
  4908. $meta['email']['attachments'] = $attachments;
  4909. // toasddrs
  4910. $meta['email']['toaddrs'] = $this->collapseLongMailingList($this->email->to_addrs);
  4911. $meta['email']['cc_addrs'] = $ccs;
  4912. // body
  4913. $description = (empty($this->email->description_html)) ? nl2br($this->email->description) : $this->email->description_html;
  4914. $meta['email']['description'] = $description;
  4915. // meta-metadata
  4916. $meta['is_sugarEmail'] = ($exMbox[0] == 'sugar') ? true : false;
  4917. if(!$meta['is_sugarEmail']) {
  4918. if($this->isAutoImport) {
  4919. $meta['is_sugarEmail'] = true;
  4920. }
  4921. } else {
  4922. if( $this->email->status != 'sent' ){
  4923. // mark SugarEmail read
  4924. $q = "UPDATE emails SET status = 'read' WHERE id = '{$uid}'";
  4925. $r = $this->db->query($q);
  4926. }
  4927. }
  4928. $return = array();
  4929. $meta['email']['name'] = to_html($this->email->name);
  4930. $meta['email']['from_addr'] = ( !empty($this->email->from_addr_name) ) ? to_html($this->email->from_addr_name) : to_html($this->email->from_addr);
  4931. $meta['email']['toaddrs'] = ( !empty($this->email->to_addrs_names) ) ? to_html($this->email->to_addrs_names) : to_html($this->email->to_addrs);
  4932. $meta['email']['cc_addrs'] = to_html($this->email->cc_addrs_names);
  4933. $meta['email']['reply_to_addr'] = to_html($this->email->reply_to_addr);
  4934. $return['meta'] = $meta;
  4935. return $return;
  4936. }
  4937. /**
  4938. * Takes a long list of email addresses from a To or CC field and shows the first 3, the rest hidden
  4939. * @param string emails
  4940. * @return string
  4941. */
  4942. function collapseLongMailingList($emails) {
  4943. global $app_strings;
  4944. $ex = explode(",", $emails);
  4945. $i = 0;
  4946. $j = 0;
  4947. if(count($ex) > 3) {
  4948. $emails = "";
  4949. $emailsHidden = "";
  4950. foreach($ex as $email) {
  4951. if($i < 2) {
  4952. if(!empty($emails)) {
  4953. $emails .= ", ";
  4954. }
  4955. $emails .= trim($email);
  4956. } else {
  4957. if(!empty($emailsHidden)) {
  4958. $emailsHidden .= ", ";
  4959. }
  4960. $emailsHidden .= trim($email);
  4961. $j++;
  4962. }
  4963. $i++;
  4964. }
  4965. if(!empty($emailsHidden)) {
  4966. $email2 = $emails;
  4967. $emails = "<span onclick='javascript:SUGAR.email2.detailView.showFullEmailList(this);' style='cursor:pointer;'>{$emails} [...{$j} {$app_strings['LBL_MORE']}]</span>";
  4968. $emailsHidden = "<span onclick='javascript:SUGAR.email2.detailView.showCroppedEmailList(this)' style='cursor:pointer; display:none;'>{$email2}, {$emailsHidden} [ {$app_strings['LBL_LESS']} ]</span>";
  4969. }
  4970. $emails .= $emailsHidden;
  4971. }
  4972. return $emails;
  4973. }
  4974. /**
  4975. * Sorts IMAP's imap_fetch_overview() results
  4976. * @param array $arr Array of standard objects
  4977. * @param string $sort Column to sort by
  4978. * @param string direction Direction to sort by (asc/desc)
  4979. * @return array Sorted array of obj.
  4980. */
  4981. function sortFetchedOverview($arr, $sort=4, $direction='DESC', $forceSeen=false) {
  4982. global $current_user;
  4983. $sortPrefs = $current_user->getPreference('folderSortOrder', 'Emails');
  4984. if(!empty($sortPrefs))
  4985. $listPrefs = $sortPrefs;
  4986. else
  4987. $listPrefs = array();
  4988. if(isset($listPrefs[$this->id][$this->mailbox])) {
  4989. $currentNode = $listPrefs[$this->id][$this->mailbox];
  4990. }
  4991. if(isset($currentNode['current']) && !empty($currentNode['current'])) {
  4992. $sort = $currentNode['current']['sort'];
  4993. $direction = $currentNode['current']['direction'];
  4994. }
  4995. // sort defaults
  4996. if(empty($sort)) {
  4997. $sort = $this->defaultSort;//4;
  4998. $direction = $this->defaultDirection; //'DESC';
  4999. } elseif(!is_numeric($sort)) {
  5000. // handle bad sort index
  5001. $sort = $this->defaultSort;
  5002. } else {
  5003. // translate numeric index to human readable
  5004. $sort = $this->hrSort[$sort];
  5005. }
  5006. if(empty($direction)) {
  5007. $direction = 'DESC';
  5008. }
  5009. $retArr = array();
  5010. $sorts = array();
  5011. foreach($arr as $k => $overview) {
  5012. $sorts['flagged'][$k] = $overview->flagged;
  5013. $sorts['status'][$k] = $overview->answered;
  5014. $sorts['from'][$k] = str_replace('"', "", $this->handleMimeHeaderDecode($overview->from));
  5015. $sorts['subj'][$k] = $this->handleMimeHeaderDecode(quoted_printable_decode($overview->subject));
  5016. $sorts['date'][$k] = $overview->date;
  5017. }
  5018. // sort by column
  5019. natcasesort($sorts[$sort]);
  5020. //_ppd($sorts[$sort]);
  5021. // direction
  5022. if(strtolower($direction) == 'desc') {
  5023. $revSorts = array();
  5024. $keys = array_reverse(array_keys($sorts[$sort]));
  5025. // _pp("count keys in DESC: ".count($keys));
  5026. // _pp("count elements in sort[sort]: ".count($sorts[$sort]));
  5027. for($i=0; $i<count($keys); $i++) {
  5028. $v = $keys[$i];
  5029. $revSorts[$v] = $sorts[$sort][$v];
  5030. }
  5031. //_pp("count post-sort: ".count($revSorts));
  5032. $sorts[$sort] = $revSorts;
  5033. }
  5034. $timedate = TimeDate::getInstance();
  5035. foreach($sorts[$sort] as $k2 => $overview2) {
  5036. $arr[$k2]->date = $timedate->fromString($arr[$k2]->date)->asDb();
  5037. $retArr[] = $arr[$k2];
  5038. }
  5039. //_pp("final count: ".count($retArr));
  5040. $finalReturn = array();
  5041. $finalReturn['retArr'] = $retArr;
  5042. $finalReturn['sortBy'] = $sort;
  5043. $finalReturn['direction'] = $direction;
  5044. return $finalReturn;
  5045. }
  5046. function setReadFlagOnFolderCache($mbox, $uid) {
  5047. global $sugar_config;
  5048. $this->mailbox = $mbox;
  5049. // cache
  5050. if($this->validCacheExists($this->mailbox)) {
  5051. $ret = $this->getCacheValue($this->mailbox);
  5052. $updates = array();
  5053. foreach($ret as $k => $v) {
  5054. if($v->imap_uid == $uid) {
  5055. $v->seen = 1;
  5056. $updates[] = $v;
  5057. break;
  5058. }
  5059. }
  5060. $this->setCacheValue($this->mailbox, array(), $updates);
  5061. }
  5062. }
  5063. /**
  5064. * Returns a list of emails in a mailbox.
  5065. * @param string mbox Name of mailbox using dot notation paths to display
  5066. * @param string $forceRefresh Flag to use cache or not
  5067. */
  5068. function displayFolderContents($mbox, $forceRefresh='false', $page) {
  5069. global $current_user;
  5070. $delimiter = $this->get_stored_options('folderDelimiter');
  5071. if ($delimiter) {
  5072. $mbox = str_replace('.', $delimiter, $mbox);
  5073. }
  5074. $this->mailbox = $mbox;
  5075. // jchi #9424, get sort and direction from user preference
  5076. $sort = 'date';
  5077. $direction = 'desc';
  5078. $sortSerial = $current_user->getPreference('folderSortOrder', 'Emails');
  5079. if(!empty($sortSerial) && !empty($_REQUEST['ieId']) && !empty($_REQUEST['mbox'])) {
  5080. $sortArray = unserialize($sortSerial);
  5081. $sort = $sortArray[$_REQUEST['ieId']][$_REQUEST['mbox']]['current']['sort'];
  5082. $direction = $sortArray[$_REQUEST['ieId']][$_REQUEST['mbox']]['current']['direction'];
  5083. }
  5084. //end
  5085. // save sort order
  5086. if(!empty($_REQUEST['sort']) && !empty($_REQUEST['dir'])) {
  5087. $this->email->et->saveListViewSortOrder($_REQUEST['ieId'], $_REQUEST['mbox'], $_REQUEST['sort'], $_REQUEST['dir']);
  5088. $sort = $_REQUEST['sort'];
  5089. $direction = $_REQUEST['dir'];
  5090. } else {
  5091. $_REQUEST['sort'] = '';
  5092. $_REQUEST['dir'] = '';
  5093. }
  5094. // cache
  5095. $ret = array();
  5096. $cacheUsed = false;
  5097. if($forceRefresh == 'false' && $this->validCacheExists($this->mailbox)) {
  5098. $emailSettings = $current_user->getPreference('emailSettings', 'Emails');
  5099. // cn: default to a low number until user specifies otherwise
  5100. if(empty($emailSettings['showNumInList'])) {
  5101. $emailSettings['showNumInList'] = 20;
  5102. }
  5103. $ret = $this->getCacheValue($this->mailbox, $emailSettings['showNumInList'], $page, $sort, $direction);
  5104. $cacheUsed = true;
  5105. }
  5106. $out = $this->displayFetchedSortedListXML($ret, $mbox);
  5107. $metadata = array();
  5108. $metadata['mbox'] = $mbox;
  5109. $metadata['ieId'] = $this->id;
  5110. $metadata['name'] = $this->name;
  5111. $metadata['fromCache'] = $cacheUsed ? 1 : 0;
  5112. $metadata['out'] = $out;
  5113. return $metadata;
  5114. }
  5115. /**
  5116. * For a group email account, create subscriptions for all users associated with the
  5117. * team assigned to the account.
  5118. *
  5119. */
  5120. function createUserSubscriptionsForGroupAccount()
  5121. {
  5122. $team = new Team();
  5123. $team->retrieve($this->team_id);
  5124. $usersList = $team->get_team_members(true);
  5125. foreach($usersList as $userObject)
  5126. {
  5127. $previousSubscriptions = unserialize(base64_decode($userObject->getPreference('showFolders', 'Emails',$userObject)));
  5128. if($previousSubscriptions === FALSE)
  5129. $previousSubscriptions = array();
  5130. $previousSubscriptions[] = $this->id;
  5131. $encodedSubs = base64_encode(serialize($previousSubscriptions));
  5132. $userObject->setPreference('showFolders',$encodedSubs , '', 'Emails');
  5133. $userObject->savePreferencesToDB();
  5134. }
  5135. }
  5136. /**
  5137. * Create a sugar folder for this inbound email account
  5138. * if the Enable Auto Import option is selected
  5139. *
  5140. * @return String Id of the sugar folder created.
  5141. */
  5142. function createAutoImportSugarFolder()
  5143. {
  5144. global $current_user;
  5145. $guid = create_guid();
  5146. $GLOBALS['log']->debug("Creating Sugar Folder for IE with id $guid");
  5147. $folder = new SugarFolder();
  5148. $folder->id = $guid;
  5149. $folder->new_with_id = TRUE;
  5150. $folder->name = $this->name;
  5151. $folder->has_child = 0;
  5152. $folder->is_group = 1;
  5153. $folder->assign_to_id = $current_user->id;
  5154. $folder->parent_folder = "";
  5155. //If this inbound email is marked as inactive, don't add subscriptions.
  5156. $addSubscriptions = ($this->status == 'Inactive' || $this->mailbox_type == 'bounce') ? FALSE : TRUE;
  5157. $folder->save($addSubscriptions);
  5158. return $guid;
  5159. }
  5160. function validCacheExists($mbox) {
  5161. $q = "SELECT count(*) c FROM email_cache WHERE ie_id = '{$this->id}'";
  5162. $r = $this->db->query($q);
  5163. $a = $this->db->fetchByAssoc($r);
  5164. $count = $a['c'];
  5165. if($count > 0) {
  5166. return true;
  5167. }
  5168. return false;
  5169. }
  5170. function displayFetchedSortedListXML($ret, $mbox) {
  5171. global $timedate;
  5172. global $current_user;
  5173. global $sugar_config;
  5174. if(empty($ret['retArr'])) {
  5175. return array();
  5176. }
  5177. $tPref = $current_user->getUserDateTimePreferences();
  5178. $return = array();
  5179. foreach($ret['retArr'] as $msg) {
  5180. $flagged = ($msg->flagged == 0) ? "" : $this->iconFlagged;
  5181. $status = ($msg->deleted) ? $this->iconDeleted : "";
  5182. $status = ($msg->draft == 0) ? $status : $this->iconDraft;
  5183. $status = ($msg->answered == 0) ? $status : $this->iconAnswered;
  5184. $from = $this->handleMimeHeaderDecode($msg->from);
  5185. $subject = $this->handleMimeHeaderDecode($msg->subject);
  5186. //$date = date($tPref['date']." ".$tPref['time'], $msg->date);
  5187. $date = $timedate->to_display_date_time($this->db->fromConvert($msg->date, 'datetime'));
  5188. //$date = date($tPref['date'], $this->getUnixHeaderDate($msg->date));
  5189. $temp = array();
  5190. $temp['flagged'] = $flagged;
  5191. $temp['status'] = $status;
  5192. $temp['from'] = to_html($from);
  5193. $temp['subject'] = $subject;
  5194. $temp['date'] = $date;
  5195. $temp['uid'] = $msg->uid; // either from an imap_search() or massaged cache value
  5196. $temp['mbox'] = $this->mailbox;
  5197. $temp['ieId'] = $this->id;
  5198. $temp['site_url'] = $sugar_config['site_url'];
  5199. $temp['seen'] = $msg->seen;
  5200. $temp['type'] = (isset($msg->type)) ? $msg->type: 'remote';
  5201. $temp['to_addrs'] = to_html($msg->to);
  5202. $temp['hasAttach'] = '0';
  5203. $return[] = $temp;
  5204. }
  5205. return $return;
  5206. }
  5207. /**
  5208. * retrieves the mailboxes for a given account in the following format
  5209. * Array(
  5210. [INBOX] => Array
  5211. (
  5212. [Bugs] => Bugs
  5213. [Builder] => Builder
  5214. [DEBUG] => Array
  5215. (
  5216. [out] => out
  5217. [test] => test
  5218. )
  5219. )
  5220. * @param bool $justRaw Default false
  5221. * @return array
  5222. */
  5223. function getMailboxes($justRaw=false) {
  5224. if($justRaw == true) {
  5225. return $this->mailboxarray;
  5226. } // if
  5227. return $this->generateMultiDimArrayFromFlatArray($this->mailboxarray, $this->retrieveDelimiter());
  5228. /*
  5229. $serviceString = $this->getConnectString('', '', false);
  5230. if(strpos($serviceString, 'pop3')) {
  5231. $obj = new temp();
  5232. $obj->name = $serviceString."INBOX";
  5233. $boxes = array("INBOX" => $obj);
  5234. } else {
  5235. $boxes = imap_getmailboxes($this->conn, $serviceString, "*");
  5236. }
  5237. $raw = array();
  5238. //_ppd($boxes);
  5239. $delimiter = '.';
  5240. // clean MBOX path names
  5241. foreach($boxes as $k => $mbox) {
  5242. $raw[] = str_replace($serviceString, "", $mbox->name);
  5243. if ($mbox->delimiter) {
  5244. $delimiter = $mbox->delimiter;
  5245. }
  5246. }
  5247. $storedOptions = unserialize(base64_decode($this->stored_options));
  5248. $storedOptions['folderDelimiter'] = $delimiter;
  5249. $this->stored_options = base64_encode(serialize($storedOptions));
  5250. $this->save();
  5251. sort($raw);
  5252. //_ppd($raw);
  5253. // used by $this->search()
  5254. if($justRaw == true) {
  5255. return $raw;
  5256. }
  5257. // generate a multi-dimensional array to iterate through
  5258. $ret = array();
  5259. foreach($raw as $mbox) {
  5260. $ret = $this->sortMailboxes($mbox, $ret, $delimiter);
  5261. }
  5262. //_ppd($ret);
  5263. return $ret;
  5264. */
  5265. }
  5266. function getMailBoxesForGroupAccount() {
  5267. $mailboxes = $this->generateMultiDimArrayFromFlatArray(explode(",", $this->mailbox), $this->retrieveDelimiter());
  5268. $mailboxesArray = $this->generateFlatArrayFromMultiDimArray($mailboxes, $this->retrieveDelimiter());
  5269. $mailboxesArray = $this->filterMailBoxFromRaw(explode(",", $this->mailbox), $mailboxesArray);
  5270. $this->saveMailBoxFolders($mailboxesArray);
  5271. /*
  5272. if ($this->mailbox != $this->$email_user) {
  5273. $mailboxes = $this->sortMailboxes($this->mailbox, $this->retrieveDelimiter());
  5274. $mailboxesArray = $this->generateFlatArrayFromMultiDimArray($mailboxes, $this->retrieveDelimiter());
  5275. $this->saveMailBoxFolders($mailboxesArray);
  5276. // save mailbox value of an inbound email account to email user
  5277. $this->saveMailBoxValueOfInboundEmail();
  5278. } else {
  5279. $mailboxes = $this->getMailboxes();
  5280. }
  5281. */
  5282. return $mailboxes;
  5283. } // fn
  5284. function saveMailBoxFolders($value) {
  5285. if (is_array($value)) {
  5286. $value = implode(",", $value);
  5287. }
  5288. $this->mailboxarray = explode(",", $value);
  5289. $value = $this->db->quoted($value);
  5290. $query = "update inbound_email set mailbox = $value where id ='{$this->id}'";
  5291. $this->db->query($query);
  5292. }
  5293. function insertMailBoxFolders($value) {
  5294. $query = "select value from config where category='InboundEmail' and name='{$this->id}'";
  5295. $r = $this->db->query($query);
  5296. $a = $this->db->fetchByAssoc($r);
  5297. if (empty($a['value'])) {
  5298. if (is_array($value)) {
  5299. $value = implode(",", $value);
  5300. }
  5301. $this->mailboxarray = explode(",", $value);
  5302. $value = $this->db->quoted($value);
  5303. $query = "INSERT INTO config VALUES('InboundEmail', '{$this->id}', $value)";
  5304. $this->db->query($query);
  5305. } // if
  5306. }
  5307. function saveMailBoxValueOfInboundEmail() {
  5308. $query = "update Inbound_email set mailbox = '{$this->email_user}'";
  5309. $this->db->query($query);
  5310. }
  5311. function retrieveMailBoxFolders() {
  5312. $this->mailboxarray = explode(",", $this->mailbox);
  5313. /*
  5314. $query = "select value from config where category='InboundEmail' and name='{$this->id}'";
  5315. $r = $this->db->query($query);
  5316. $a = $this->db->fetchByAssoc($r);
  5317. $this->mailboxarray = explode(",", $a['value']);
  5318. */
  5319. } // fn
  5320. function retrieveDelimiter() {
  5321. $delimiter = $this->get_stored_options('folderDelimiter');
  5322. if (!$delimiter) {
  5323. $delimiter = '.';
  5324. }
  5325. return $delimiter;
  5326. } // fn
  5327. function generateFlatArrayFromMultiDimArray($arraymbox, $delimiter) {
  5328. $ret = array();
  5329. foreach($arraymbox as $key => $value) {
  5330. $this->generateArrayData($key, $value, $ret, $delimiter);
  5331. } // foreach
  5332. return $ret;
  5333. } // fn
  5334. function generateMultiDimArrayFromFlatArray($raw, $delimiter) {
  5335. // generate a multi-dimensional array to iterate through
  5336. $ret = array();
  5337. foreach($raw as $mbox) {
  5338. $ret = $this->sortMailboxes($mbox, $ret, $delimiter);
  5339. }
  5340. return $ret;
  5341. } // fn
  5342. function generateArrayData($key, $arraymbox, &$ret, $delimiter) {
  5343. $ret [] = $key;
  5344. if (is_array($arraymbox)) {
  5345. foreach($arraymbox as $mboxKey => $value) {
  5346. $newKey = $key . $delimiter . $mboxKey;
  5347. $this->generateArrayData($newKey, $value, $ret, $delimiter);
  5348. } // foreach
  5349. } // if
  5350. }
  5351. /**
  5352. * sorts the folders in a mailbox in a multi-dimensional array
  5353. * @param string $MBOX
  5354. * @param array $ret
  5355. * @return array
  5356. */
  5357. function sortMailboxes($mbox, $ret, $delimeter = ".") {
  5358. if(strpos($mbox, $delimeter)) {
  5359. $node = substr($mbox, 0, strpos($mbox, $delimeter));
  5360. $nodeAfter = substr($mbox, strpos($mbox, $node) + strlen($node) + 1, strlen($mbox));
  5361. if(!isset($ret[$node])) {
  5362. $ret[$node] = array();
  5363. } elseif(isset($ret[$node]) && !is_array($ret[$node])) {
  5364. $ret[$node] = array();
  5365. }
  5366. $ret[$node] = $this->sortMailboxes($nodeAfter, $ret[$node], $delimeter);
  5367. } else {
  5368. $ret[$mbox] = $mbox;
  5369. }
  5370. return $ret;
  5371. }
  5372. /**
  5373. * parses Sugar's storage method for imap server service strings
  5374. * @return string
  5375. */
  5376. function getServiceString() {
  5377. $service = '';
  5378. $exServ = explode('::', $this->service);
  5379. foreach($exServ as $v) {
  5380. if(!empty($v) && ($v != 'imap' && $v !='pop3')) {
  5381. $service .= '/'.$v;
  5382. }
  5383. }
  5384. return $service;
  5385. }
  5386. /**
  5387. * Get Email messages IDs from server which aren't in database
  5388. * @return array Ids of messages, which aren't still in database
  5389. */
  5390. public function getNewEmailsForSyncedMailbox()
  5391. {
  5392. // ids's count limit for batch processing
  5393. $limit = 20;
  5394. $msgIds = imap_search($this->conn, 'ALL UNDELETED');
  5395. $result = array();
  5396. try{
  5397. if(count($msgIds) > 0)
  5398. {
  5399. /*
  5400. * @var collect results of queries and message headers
  5401. */
  5402. $tmpMsgs = array();
  5403. $repeats = 0;
  5404. $counter = 0;
  5405. // sort IDs to get lastest on top
  5406. arsort($msgIds);
  5407. $GLOBALS['log']->debug('-----> getNewEmailsForSyncedMailbox() got '.count($msgIds).' Messages');
  5408. foreach($msgIds as $k => &$msgNo)
  5409. {
  5410. $uid = imap_uid($this->conn, $msgNo);
  5411. $header = imap_headerinfo($this->conn, $msgNo);
  5412. $fullHeader = imap_fetchheader($this->conn, $msgNo);
  5413. $message_id = $header->message_id;
  5414. $deliveredTo = $this->id;
  5415. $matches = array();
  5416. preg_match('/(delivered-to:|x-real-to:){1}\s*(\S+)\s*\n{1}/im', $fullHeader, $matches);
  5417. if(count($matches))
  5418. {
  5419. $deliveredTo = $matches[2];
  5420. }
  5421. if(empty($message_id) || !isset($message_id))
  5422. {
  5423. $GLOBALS['log']->debug('*********** NO MESSAGE_ID.');
  5424. $message_id = $this->getMessageId($header);
  5425. }
  5426. // generate compound messageId
  5427. $this->compoundMessageId = trim($message_id) . trim($deliveredTo);
  5428. // if the length > 255 then md5 it so that the data will be of smaller length
  5429. if (strlen($this->compoundMessageId) > 255)
  5430. {
  5431. $this->compoundMessageId = md5($this->compoundMessageId);
  5432. } // if
  5433. if (empty($this->compoundMessageId))
  5434. {
  5435. break;
  5436. } // if
  5437. $counter++;
  5438. $potentials = clean_xss($this->compoundMessageId, false);
  5439. if(is_array($potentials) && !empty($potentials))
  5440. {
  5441. foreach($potentials as $bad)
  5442. {
  5443. $this->compoundMessageId = str_replace($bad, "", $this->compoundMessageId);
  5444. }
  5445. }
  5446. array_push($tmpMsgs, array('msgNo' => $msgNo, 'msgId' => $this->compoundMessageId, 'exists' => 0));
  5447. if($counter == $limit)
  5448. {
  5449. $counter = 0;
  5450. $query = array();
  5451. foreach(array_slice($tmpMsgs, -$limit, $limit) as $k1 => $v1)
  5452. {
  5453. $query[] = $v1['msgId'];
  5454. }
  5455. $query = 'SELECT count(emails.message_id) as cnt, emails.message_id AS mid FROM emails WHERE emails.message_id IN ("' . implode('","', $query) . '") and emails.deleted = 0 group by emails.message_id';
  5456. $r = $this->db->query($query);
  5457. $tmp = array();
  5458. while($a = $this->db->fetchByAssoc($r))
  5459. {
  5460. $tmp[html_entity_decode($a['mid'])] = $a['cnt'];
  5461. }
  5462. foreach($tmpMsgs as $k1 => $v1)
  5463. {
  5464. if(isset($tmp[$v1['msgId']]) && $tmp[$v1['msgId']] > 0)
  5465. {
  5466. $tmpMsgs[$k1]['exists'] = 1;
  5467. }
  5468. }
  5469. foreach($tmpMsgs as $k1 => $v1)
  5470. {
  5471. if($v1['exists'] == 0)
  5472. {
  5473. $repeats = 0;
  5474. array_push($result, $v1['msgNo']);
  5475. }else{
  5476. $repeats++;
  5477. }
  5478. }
  5479. if($repeats > 0)
  5480. {
  5481. if($repeats >= $limit)
  5482. {
  5483. break;
  5484. }
  5485. else
  5486. {
  5487. $tmpMsgs = array_splice($tmpMsgs, -$repeats, $repeats);
  5488. }
  5489. }
  5490. else
  5491. {
  5492. $tmpMsgs = array();
  5493. }
  5494. }
  5495. }
  5496. unset($msgNo);
  5497. }
  5498. }catch(Exception $ex)
  5499. {
  5500. $GLOBALS['log']->fatal($ex->getMessage());
  5501. }
  5502. $GLOBALS['log']->debug('-----> getNewEmailsForSyncedMailbox() got '.count($result).' unsynced messages');
  5503. return $result;
  5504. }
  5505. } // end class definition
  5506. /**
  5507. * Simple class to mirror the passed object from an imap_fetch_overview() call
  5508. */
  5509. class Overview {
  5510. var $subject;
  5511. var $from;
  5512. var $fromaddr;
  5513. var $to;
  5514. var $toaddr;
  5515. var $date;
  5516. var $message_id;
  5517. var $size;
  5518. var $uid;
  5519. var $msgno;
  5520. var $recent;
  5521. var $flagged;
  5522. var $answered;
  5523. var $deleted;
  5524. var $seen;
  5525. var $draft;
  5526. var $indices; /* = array(
  5527. array(
  5528. 'name' => 'mail_date',
  5529. 'type' => 'index',
  5530. 'fields' => array(
  5531. 'mbox',
  5532. 'senddate',
  5533. )
  5534. ),
  5535. array(
  5536. 'name' => 'mail_from',
  5537. 'type' => 'index',
  5538. 'fields' => array(
  5539. 'mbox',
  5540. 'fromaddr',
  5541. )
  5542. ),
  5543. array(
  5544. 'name' => 'mail_subj',
  5545. 'type' => 'index',
  5546. 'fields' => array(
  5547. 'mbox',
  5548. 'subject',
  5549. )
  5550. ),
  5551. );
  5552. */
  5553. var $fieldDefs;/* = array(
  5554. 'mbox' => array(
  5555. 'name' => 'mbox',
  5556. 'type' => 'varchar',
  5557. 'len' => 60,
  5558. 'required' => true,
  5559. ),
  5560. 'subject' => array(
  5561. 'name' => 'subject',
  5562. 'type' => 'varchar',
  5563. 'len' => 100,
  5564. 'required' => false,
  5565. ),
  5566. 'fromaddr' => array(
  5567. 'name' => 'fromaddr',
  5568. 'type' => 'varchar',
  5569. 'len' => 100,
  5570. 'required' => true,
  5571. ),
  5572. 'toaddr' => array(
  5573. 'name' => 'toaddr',
  5574. 'type' => 'varchar',
  5575. 'len' => 100,
  5576. 'required' => true,
  5577. ),
  5578. 'senddate' => array(
  5579. 'name' => 'senddate',
  5580. 'type' => 'datetime',
  5581. 'required' => true,
  5582. ),
  5583. 'message_id' => array(
  5584. 'name' => 'message_id',
  5585. 'type' => 'varchar',
  5586. 'len' => 255,
  5587. 'required' => false,
  5588. ),
  5589. 'mailsize' => array(
  5590. 'name' => 'mailsize',
  5591. 'type' => 'uint',
  5592. 'len' => 16,
  5593. 'required' => true,
  5594. ),
  5595. 'uid' => array(
  5596. 'name' => 'uid',
  5597. 'type' => 'uint',
  5598. 'len' => 32,
  5599. 'required' => true,
  5600. ),
  5601. 'msgno' => array(
  5602. 'name' => 'msgno',
  5603. 'type' => 'uint',
  5604. 'len' => 32,
  5605. 'required' => false,
  5606. ),
  5607. 'recent' => array(
  5608. 'name' => 'recent',
  5609. 'type' => 'tinyint',
  5610. 'len' => 1,
  5611. 'required' => true,
  5612. ),
  5613. 'flagged' => array(
  5614. 'name' => 'flagged',
  5615. 'type' => 'tinyint',
  5616. 'len' => 1,
  5617. 'required' => true,
  5618. ),
  5619. 'answered' => array(
  5620. 'name' => 'answered',
  5621. 'type' => 'tinyint',
  5622. 'len' => 1,
  5623. 'required' => true,
  5624. ),
  5625. 'deleted' => array(
  5626. 'name' => 'deleted',
  5627. 'type' => 'tinyint',
  5628. 'len' => 1,
  5629. 'required' => true,
  5630. ),
  5631. 'seen' => array(
  5632. 'name' => 'seen',
  5633. 'type' => 'tinyint',
  5634. 'len' => 1,
  5635. 'required' => true,
  5636. ),
  5637. 'draft' => array(
  5638. 'name' => 'draft',
  5639. 'type' => 'tinyint',
  5640. 'len' => 1,
  5641. 'required' => true,
  5642. ),
  5643. );
  5644. */
  5645. function Overview() {
  5646. global $dictionary;
  5647. if(!isset($dictionary['email_cache']) || empty($dictionary['email_cache'])) {
  5648. if(file_exists('custom/metadata/email_cacheMetaData.php')) {
  5649. include('custom/metadata/email_cacheMetaData.php');
  5650. } else {
  5651. include('metadata/email_cacheMetaData.php');
  5652. }
  5653. }
  5654. $this->fieldDefs = $dictionary['email_cache']['fields'];
  5655. $this->indices = $dictionary['email_cache']['indices'];
  5656. }
  5657. }