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-2013 SugarCRM Inc.
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.
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
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
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.
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.
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 ********************************************************************************/
39 require_once('include/OutboundEmail/OutboundEmail.php');
41 function this_callback($str) {
42 foreach($str as $match) {
43 $ret .= chr(hexdec(str_replace("%","",$match)));
49 * Stub for certain interactions;
55 class InboundEmail extends SugarBean {
58 var $purifier; // HTMLPurifier object placeholder
66 var $modified_user_id;
69 var $modified_by_name;
89 var $outboundInstance; // id to outbound_email instance
91 var $iconFlagged = "F";
93 var $iconAnswered = "A";
94 var $iconDeleted = "del";
95 var $isAutoImport = false;
97 var $attachmentCount = 0;
98 var $tempAttachment = array();
99 var $unsafeChars = array("&", "!", "'", '"', '\\', '/', '<', '>', '|', '$',);
101 var $defaultSort = 'date';
102 var $defaultDirection = "DESC";
110 var $hrSortLocal = array(
111 'flagged' => 'flagged',
112 'status' => 'answered',
113 'from' => 'fromaddr',
114 'subject' => 'subject',
115 'date' => 'senddate',
118 // default attributes
119 var $transferEncoding = array(0 => '7BIT',
123 4 => 'QUOTED-PRINTABLE',
127 var $compoundMessageId; // concatenation of messageID and deliveredToEmail
128 var $serverConnectString;
129 var $disable_row_level_security = true;
130 var $InboundEmailCachePath;
131 var $InboundEmailCacheFile = 'InboundEmail.cache.php';
132 var $object_name = 'InboundEmail';
133 var $module_dir = 'InboundEmail';
134 var $table_name = 'inbound_email';
135 var $new_schema = true;
136 var $process_save_dates = true;
141 var $required_fields = array('name' => 'name',
142 'server_url' => 'server_url',
143 'mailbox' => 'mailbox',
147 var $imageTypes = array("JPG", "JPEG", "GIF", "PNG");
148 var $inlineImages = array(); // temporary space to store ID of inlined images
149 var $defaultEmailNumAutoreplies24Hours = 10;
150 var $maxEmailNumAutoreplies24Hours = 10;
151 // custom ListView attributes
152 var $mailbox_type_name;
153 var $global_personal_string;
154 // service attributes
159 var $keyForUsersDefaultIEAccount = 'defaultIEAccount';
160 // prefix to use when importing inlinge images in emails
166 function InboundEmail() {
167 $this->InboundEmailCachePath = sugar_cached('modules/InboundEmail');
168 $this->EmailCachePath = sugar_cached('modules/Emails');
170 if(function_exists("imap_timeout")) {
182 $this->smarty = new Sugar_Smarty();
183 $this->overview = new Overview();
184 $this->imagePrefix = "{$GLOBALS['sugar_config']['site_url']}/cache/images/";
190 * @return object Bean
192 function retrieve($id, $encode=true, $deleted=true) {
193 $ret = parent::retrieve($id,$encode,$deleted);
195 if (!is_null($ret)) {
196 $this->email_password = blowfishDecode(blowfishGetKey('InboundEmail'), $this->email_password);
197 $this->retrieveMailBoxFolders();
203 * wraps SugarBean->save()
204 * @param string ID of saved bean
206 function save($check_notify=false) {
207 // generate cache table for email 2.0
208 $multiDImArray = $this->generateMultiDimArrayFromFlatArray(explode(",", $this->mailbox), $this->retrieveDelimiter());
209 $raw = $this->generateFlatArrayFromMultiDimArray($multiDImArray, $this->retrieveDelimiter());
211 //_pp(explode(",", $this->mailbox));
213 $raw = $this->filterMailBoxFromRaw(explode(",", $this->mailbox), $raw);
214 $this->mailbox = implode(",", $raw);
215 if(!empty($this->email_password)) {
216 $this->email_password = blowfishEncode(blowfishGetKey('InboundEmail'), $this->email_password);
218 $ret = parent::save($check_notify);
222 function filterMailBoxFromRaw($mailboxArray, $rawArray) {
223 $newArray = array_intersect($mailboxArray, $rawArray);
229 * Overrides SugarBean's mark_deleted() to drop the related cache table
230 * @param string $id GUID of I-E instance
232 function mark_deleted($id) {
233 parent::mark_deleted($id);
235 //bug52021 we need to keep the reference to the folders table in order for emails module to function properly
236 //$q = "update inbound_email set groupfolder_id = null WHERE id = '{$id}'";
237 //$r = $this->db->query($q);
238 $this->deleteCache();
242 * Mark cached email answered (replied)
243 * @param string $mailid (uid for imap, message_id for pop3)
245 function mark_answered($mailid, $type = 'smtp') {
248 $q = "update email_cache set answered = 1 WHERE imap_uid = $mailid and ie_id = '{$this->id}'";
249 $this->db->query($q);
252 $q = "update email_cache set answered = 1 WHERE message_id = '$mailid' and ie_id = '{$this->id}'";
253 $this->db->query($q);
259 * Renames an IMAP mailbox
260 * @param string $newName
262 function renameFolder($oldName, $newName) {
263 //$this->mailbox = "INBOX"
264 $this->connectMailserver();
265 $oldConnect = $this->getConnectString('', $oldName);
266 $newConnect = $this->getConnectString('', $newName);
267 if(!imap_renamemailbox($this->conn, $oldConnect , $newConnect)) {
268 $GLOBALS['log']->debug("***INBOUNDEMAIL: failed to rename mailbox [ {$oldConnect} ] to [ {$newConnect} ]");
270 $this->mailbox = str_replace($oldName, $newName, $this->mailbox);
272 $sessionFoldersString = $this->getSessionInboundFoldersString($this->server_url, $this->email_user, $this->port, $this->protocol);
273 $sessionFoldersString = str_replace($oldName, $newName, $sessionFoldersString);
274 $this->setSessionInboundFoldersString($this->server_url, $this->email_user, $this->port, $this->protocol, $sessionFoldersString);
279 ///////////////////////////////////////////////////////////////////////////
280 //// CUSTOM LOGIC HOOKS
282 * Called from $this->getMessageText()
283 * Allows upgrade-safe custom processing of message text.
286 * 1. Create a directory path: ./custom/modules/InboundEmail if it does not exist
287 * 2. Create a file in the ./custom/InboundEmail/ folder called "getMessageText.php"
288 * 3. Define a function named "custom_getMessageText()" that takes a string as an argument and returns a string
290 * @param string $msgPart
293 function customGetMessageText($msgPart) {
294 $custom = "custom/modules/InboundEmail/getMessageText.php";
296 if(file_exists($custom)) {
297 include_once($custom);
299 if(function_exists("custom_getMessageText")) {
300 $GLOBALS['log']->debug("*** INBOUND EMAIL-CUSTOM_LOGIC: calling custom_getMessageText()");
301 $msgPart = custom_getMessageText($msgPart);
307 //// END CUSTOM LOGIC HOOKS
308 ///////////////////////////////////////////////////////////////////////////
312 ///////////////////////////////////////////////////////////////////////////
313 //// EMAIL 2.0 SPECIFIC
315 * constructs a nicely formatted version of raw source
316 * @param int $uid UID of email
319 function getFormattedRawSource($uid) {
322 //if($this->protocol == 'pop3') {
323 //$raw = $app_strings['LBL_EMAIL_VIEW_UNSUPPORTED'];
325 if (empty($this->id)) {
326 $q = "SELECT raw_source FROM emails_text WHERE email_id = '{$uid}'";
327 $r = $this->db->query($q);
328 $a = $this->db->fetchByAssoc($r);
330 $raw = $this->convertToUtf8($a['raw_source']);
332 $raw = $app_strings['LBL_EMAIL_ERROR_VIEW_RAW_SOURCE'];
335 if ($this->isPop3Protocol()) {
336 $uid = $this->getCorrectMessageNoForPop3($uid);
338 $raw = imap_fetchheader($this->conn, $uid, FT_UID+FT_PREFETCHTEXT);
339 $raw .= $this->convertToUtf8(imap_body($this->conn, $uid, FT_UID));
341 $raw = to_html($raw);
350 * helper method to convert text to utf-8 if necessary
352 * @param string $input text
353 * @return string output text
355 function convertToUtf8($input)
357 $charset = $GLOBALS['locale']->detectCharset($input, true);
359 // we haven't a clue due to missing package?, just return as itself
360 if ($charset === FALSE) {
364 // convert if we can or must
365 return $this->handleCharsetTranslation($input, $charset);
369 * constructs a nicely formatted version of email headers.
373 function getFormattedHeaders($uid) {
376 //if($this->protocol == 'pop3') {
377 // $header = $app_strings['LBL_EMAIL_VIEW_UNSUPPORTED'];
379 if ($this->isPop3Protocol()) {
380 $uid = $this->getCorrectMessageNoForPop3($uid);
382 $headers = imap_fetchheader($this->conn, $uid, FT_UID);
384 $lines = explode("\n", $headers);
386 $header = "<table cellspacing='0' cellpadding='2' border='0' width='100%'>";
388 foreach($lines as $line) {
392 $key = trim(substr($line, 0, strpos($line, ":")));
393 $key = strip_tags($key);
394 $value = trim(substr($line, strpos($line, ":") + 1));
395 $value = to_html($value);
398 $header .= "<td class='displayEmailLabel' NOWRAP><b>{$key}</b> </td>";
399 $header .= "<td class='displayEmailValueWhite'>{$value} </td>";
404 $header .= "</table>";
410 * Empties Trash folders
412 function emptyTrash() {
413 global $sugar_config;
415 $this->mailbox = $this->get_stored_options("trashFolder");
416 if (empty($this->mailbox)) {
417 $this->mailbox = 'INBOX.Trash';
419 $this->connectMailserver();
421 $uids = imap_search($this->conn, "ALL", SE_UID);
423 foreach($uids as $uid) {
424 if(!imap_delete($this->conn, $uid, FT_UID)) {
425 $lastError = imap_last_error();
426 $GLOBALS['log']->warn("INBOUNDEMAIL: emptyTrash() Could not delete message [ {$uid} ] from [ {$this->mailbox} ]. IMAP_ERROR [ {$lastError} ]");
430 // remove local cache file
431 $q = "DELETE FROM email_cache WHERE mbox = '{$this->mailbox}' AND ie_id = '{$this->id}'";
432 $r = $this->db->query($q);
436 * Fetches a timestamp
438 function getCacheTimestamp($mbox) {
439 $key = $this->db->quote("{$this->id}_{$mbox}");
440 $q = "SELECT ie_timestamp FROM inbound_email_cache_ts WHERE id = '{$key}'";
441 $r = $this->db->query($q);
442 $a = $this->db->fetchByAssoc($r);
447 return $a['ie_timestamp'];
451 * sets the cache timestamp
454 function setCacheTimestamp($mbox) {
455 $key = $this->db->quote("{$this->id}_{$mbox}");
457 $tsOld = $this->getCacheTimestamp($mbox);
460 $q = "INSERT INTO inbound_email_cache_ts (id, ie_timestamp) VALUES ('{$key}', {$ts})";
462 $q = "UPDATE inbound_email_cache_ts SET ie_timestamp = {$ts} WHERE id = '{$key}'";
465 $r = $this->db->query($q, true);
466 $GLOBALS['log']->info("INBOUNDEMAIL-CACHE: setting timestamp query [ {$q} ]");
471 * Gets a count of all rows that are flagged seen = 0
472 * @param string $mbox
475 function getCacheUnreadCount($mbox) {
476 $q = "SELECT count(*) c FROM email_cache WHERE mbox = '{$mbox}' AND seen = 0 AND ie_id = '{$this->id}'";
477 $r = $this->db->query($q);
478 $a = $this->db->fetchByAssoc($r);
484 * Returns total number of emails for a mailbox
488 function getCacheCount($mbox) {
489 $q = "SELECT count(*) c FROM email_cache WHERE mbox = '{$mbox}' AND ie_id = '{$this->id}'";
490 $r = $this->db->query($q);
491 $a = $this->db->fetchByAssoc($r);
496 function getCacheUnread($mbox) {
497 $q = "SELECT count(*) c FROM email_cache WHERE mbox = '{$mbox}' AND ie_id = '{$this->id}' AND seen = '0'";
498 $r = $this->db->query($q);
499 $a = $this->db->fetchByAssoc($r);
506 * Deletes all rows for a given instance
508 function deleteCache() {
509 $q = "DELETE FROM email_cache WHERE ie_id = '{$this->id}'";
511 $GLOBALS['log']->info("INBOUNDEMAIL: deleting cache using query [ {$q} ]");
513 $r = $this->db->query($q);
517 * Deletes all the pop3 data which has been deleted from server
519 function deletePop3Cache() {
520 global $sugar_config;
521 $UIDLs = $this->pop3_getUIDL();
522 $cacheUIDLs = $this->pop3_getCacheUidls();
523 foreach($cacheUIDLs as $msgNo => $msgId) {
524 if (!in_array($msgId, $UIDLs)) {
525 $md5msgIds = md5($msgId);
526 $file = "{$this->EmailCachePath}/{$this->id}/messages/INBOX{$md5msgIds}.PHP";
527 $GLOBALS['log']->debug("INBOUNDEMAIL: deleting file [ {$file} ] ");
528 if(file_exists($file)) {
530 $GLOBALS['log']->debug("INBOUNDEMAIL: Could not delete [ {$file} ] ");
533 $q = "DELETE from email_cache where imap_uid = {$msgNo} AND msgno = {$msgNo} AND ie_id = '{$this->id}' AND message_id = '{$msgId}'";
534 $r = $this->db->query($q);
540 * Retrieves cached headers
543 function getCacheValueForUIDs($mbox, $UIDs) {
544 if (!is_array($UIDs) || empty($UIDs)) {
548 $q = "SELECT * FROM email_cache WHERE ie_id = '{$this->id}' AND mbox = '{$mbox}' AND ";
552 $slicedArray = array_slice($UIDs, $startIndex ,$endIndex);
553 $columnName = ($this->isPop3Protocol() ? "message_id" : "imap_uid");
555 'timestamp' => $this->getCacheTimestamp($mbox),
559 while (!empty($slicedArray)) {
560 $messageIdString = implode(',', $slicedArray);
561 $GLOBALS['log']->debug("sliced array = {$messageIdString}");
562 $extraWhere = "{$columnName} IN (";
564 foreach($slicedArray as $UID) {
566 $extraWhere = $extraWhere . ",";
569 $extraWhere = "{$extraWhere} '{$UID}'";
571 $newQuery = $q . $extraWhere . ")";
572 $r = $this->db->query($newQuery);
574 while($a = $this->db->fetchByAssoc($r)) {
575 if (isset($a['uid'])) {
576 if ($this->isPop3Protocol()) {
577 $ret['uids'][] = $a['message_id'];
579 $ret['uids'][] = $a['uid'];
583 $overview = new Overview();
585 foreach($a as $k => $v) {
589 $overview->imap_uid = $v;
590 if ($this->isPop3Protocol()) {
591 $overview->uid = $a['message_id'];
597 $overview->to = from_html($v);
601 $overview->from = from_html($v);
605 $overview->size = $v;
609 $overview->date = $v;
613 $overview->$k = from_html($v);
617 $ret['retArr'][] = $overview;
619 $startIndex = $startIndex + $endIndex;
620 $slicedArray = array_slice($UIDs, $startIndex ,$endIndex);
621 $messageIdString = implode(',', $slicedArray);
622 $GLOBALS['log']->debug("sliced array = {$messageIdString}");
628 * Retrieves cached headers
631 function getCacheValue($mbox, $limit = 20, $page = 1, $sort='', $direction='') {
632 // try optimizing this call as we don't want repeat queries
633 if(!empty($this->currentCache)) {
634 return $this->currentCache;
637 $sort = (empty($sort)) ? $this->defaultSort : $sort;
638 if (!in_array(strtolower($direction), array('asc', 'desc'))) {
639 $direction = $this->defaultDirection;
642 if (!empty($this->hrSortLocal[$sort])) {
643 $order = " ORDER BY {$this->hrSortLocal[$sort]} {$direction}";
648 $q = "SELECT * FROM email_cache WHERE ie_id = '{$this->id}' AND mbox = '{$mbox}' {$order}";
651 $start = ( $page - 1 ) * $limit;
652 $r = $this->db->limitQuery($q, $start, $limit);
654 $r = $this->db->query($q);
658 'timestamp' => $this->getCacheTimestamp($mbox),
663 while($a = $this->db->fetchByAssoc($r)) {
664 if (isset($a['uid'])) {
665 if ($this->isPop3Protocol()) {
666 $ret['uids'][] = $a['message_id'];
668 $ret['uids'][] = $a['uid'];
672 $overview = new Overview();
674 foreach($a as $k => $v) {
678 $overview->imap_uid = $v;
679 if ($this->isPop3Protocol()) {
680 $overview->uid = $a['message_id'];
686 $overview->to = from_html($v);
690 $overview->from = from_html($v);
694 $overview->size = $v;
698 $overview->date = $v;
702 $overview->$k = from_html($v);
706 $ret['retArr'][] = $overview;
709 $this->currentCache = $ret;
717 function setCacheValue($mbox, $insert, $update=array(), $remove=array()) {
724 // reset in-memory cache
725 $this->currentCache = null;
727 $table = 'email_cache';
728 $where = "WHERE ie_id = '{$this->id}' AND mbox = '{$mbox}'";
730 // handle removed rows
731 if(!empty($remove)) {
733 foreach($remove as $overview) {
734 if(!empty($removeIds)) {
738 $removeIds .= "'{$overview->imap_uid}'";
741 $q = "DELETE FROM {$table} {$where} AND imap_uid IN ({$removeIds})";
743 $GLOBALS['log']->info("INBOUNDEMAIL-CACHE: delete query [ {$q} ]");
745 $r = $this->db->query($q, true, $q);
748 // handle insert rows
749 if(!empty($insert)) {
750 $q = "SELECT imap_uid FROM {$table} {$where}";
751 $GLOBALS['log']->info("INBOUNDEMAIL-CACHE: filter UIDs query [ {$q} ]");
752 $r = $this->db->query($q);
755 while($a = $this->db->fetchByAssoc($r)) {
756 $uids[] = $a['imap_uid'];
758 $count = count($uids);
759 $GLOBALS['log']->info("INBOUNDEMAIL-CACHE: found [ {$count} ] UIDs to filter against");
762 foreach($uids as $uid) {
767 $GLOBALS['log']->info("INBOUNDEMAIL-CACHE: filter UIDs: [ {$tmp} ]");
771 foreach($this->overview->fieldDefs as $colDef) {
775 $cols .= "{$colDef['name']}";
777 foreach($insert as $overview) {
778 if(in_array($overview->imap_uid, $uids))
780 // fixing bug #49543: setting 'mbox' property for the following updating of other items in this box
781 if (!isset($overview->mbox))
783 $overview->mbox = $mbox;
785 $update[] = $overview;
791 foreach($this->overview->fieldDefs as $colDef) {
792 if(!empty($values)) {
796 // trim values for Oracle/MSSql
797 if( isset($colDef['len']) && !empty($colDef['len']) &&
798 isset($colDef['type']) && !empty($colDef['type']) &&
799 $colDef['type'] == 'varchar'
802 if (isset($overview->$colDef['name']))
804 $overview->$colDef['name'] = substr($overview->$colDef['name'], 0, $colDef['len']);
808 switch($colDef['name']) {
810 if(isset($overview->uid) && !empty($overview->uid)) {
811 $this->imap_uid = $overview->uid;
813 $values .= "'{$this->imap_uid}'";
817 $values .= "'{$this->id}'";
821 $values .= $this->db->quoted($overview->to);
825 $values .= $this->db->quoted($overview->from);
829 $values .= $this->db->quoted($overview->message_id);
833 $values .= $overview->size;
837 $conv=$timedate->fromString($overview->date);
839 $values .= $this->db->quoted($conv->asDb());
846 $values .= "'{$mbox}'";
850 $overview->$colDef['name'] = SugarCleaner::cleanHtml(from_html($overview->$colDef['name']));
851 $values .= $this->db->quoted($overview->$colDef['name']);
856 $q = "INSERT INTO {$table} ({$cols}) VALUES ({$values})";
857 $GLOBALS['log']->info("INBOUNDEMAIL-CACHE: insert query [ {$q} ]");
858 $r = $this->db->query($q, true, $q);
862 // handle update rows
863 if(!empty($update)) {
865 foreach($this->overview->fieldDefs as $colDef) {
869 $cols .= "{$colDef['name']}";
872 foreach($update as $overview) {
873 $q = "UPDATE {$table} SET ";
876 foreach($this->overview->fieldDefs as $colDef) {
878 switch($colDef['name']) {
893 if (isset($overview->$colDef['name']))
895 $value = $this->db->quoted($overview->$colDef['name']);
899 $value = $this->db->quoted($value);
901 $set .= "{$colDef['name']} = " . $value;
906 $q .= $set . " WHERE ie_id = '{$this->id}' AND mbox = '{$overview->mbox}' AND imap_uid = '{$overview->imap_uid}'";
907 $GLOBALS['log']->info("INBOUNDEMAIL-CACHE: update query [ {$q} ]");
908 $r = $this->db->query($q, true, $q);
915 * Opens a socket connection to the pop3 server
918 function pop3_open() {
919 if(!is_resource($this->pop3socket)) {
920 $GLOBALS['log']->info("*** INBOUNDEMAIL: opening socket connection");
921 $exServ = explode('::', $this->service);
922 $socket = ($exServ[2] == 'ssl') ? "ssl://" : "tcp://";
923 $socket .= $this->server_url;
924 $this->pop3socket = fsockopen($socket, $this->port);
926 $GLOBALS['log']->info("*** INBOUNDEMAIL: REUSING socket connection");
930 if(!is_resource($this->pop3socket)) {
931 $GLOBALS['log']->debug("*** INBOUNDEMAIL: unable to open socket connection");
936 $ret = trim(fgets($this->pop3socket, 1024));
937 $GLOBALS['log']->info("*** INBOUNDEMAIL: got socket connection [ {$ret} ]");
942 * Closes connections and runs clean-up routines
944 function pop3_cleanUp() {
945 $GLOBALS['log']->info("*** INBOUNDEMAIL: cleaning up socket connection");
946 fputs($this->pop3socket, "QUIT\r\n");
947 $buf = fgets($this->pop3socket, 1024);
948 fclose($this->pop3socket);
952 * sends a command down to the POP3 server
953 * @param string command
958 function pop3_sendCommand($command, $args='', $return=true) {
959 $command .= " {$args}";
960 $command = trim($command);
961 $GLOBALS['log']->info("*** INBOUNDEMAIL: pop3_sendCommand() SEND [ {$command} ]");
964 fputs($this->pop3socket, $command);
967 $ret = trim(fgets($this->pop3socket, 1024));
968 $GLOBALS['log']->info("*** INBOUNDEMAIL: pop3_sendCommand() RECEIVE [ {$ret} ]");
973 function getPop3NewMessagesToDownload() {
974 $pop3UIDL = $this->pop3_getUIDL();
975 $cacheUIDLs = $this->pop3_getCacheUidls();
976 // new email cache values we should deal with
977 $diff = array_diff_assoc($pop3UIDL, $cacheUIDLs);
978 // this is msgNo to UIDL array
979 $diff = $this->pop3_shiftCache($diff, $cacheUIDLs);
980 // get all the keys which are msgnos;
981 return array_keys($diff);
984 function getPop3NewMessagesToDownloadForCron() {
985 $pop3UIDL = $this->pop3_getUIDL();
986 $cacheUIDLs = $this->pop3_getCacheUidls();
987 // new email cache values we should deal with
988 $diff = array_diff_assoc($pop3UIDL, $cacheUIDLs);
989 // this is msgNo to UIDL array
990 $diff = $this->pop3_shiftCache($diff, $cacheUIDLs);
991 // insert data into email_cache
992 if ($this->groupfolder_id != null && $this->groupfolder_id != "" && $this->isPop3Protocol()) {
993 $searchResults = array_keys($diff);
994 $concatResults = implode(",", $searchResults);
995 if ($this->connectMailserver() == 'true') {
996 $fetchedOverviews = imap_fetch_overview($this->conn, $concatResults);
997 // clean up cache entry
998 foreach($fetchedOverviews as $k => $overview) {
999 $overview->message_id = trim($diff[$overview->msgno]);
1000 $fetchedOverviews[$k] = $overview;
1002 $this->updateOverviewCacheFile($fetchedOverviews);
1009 * This method returns all the UIDL for this account. This should be called if the protocol is pop3
1010 * @return array od messageno to UIDL array
1012 function pop3_getUIDL() {
1014 if($this->pop3_open()) {
1016 $this->pop3_sendCommand("USER", $this->email_user);
1017 $this->pop3_sendCommand("PASS", $this->email_password);
1020 $this->pop3_sendCommand("UIDL", '', false); // leave socket buffer alone until the while()
1021 fgets($this->pop3socket, 1024); // handle "OK+";
1026 if(is_resource($this->pop3socket)) {
1027 while(!feof($this->pop3socket)) {
1028 $buf = fgets($this->pop3socket, 1024); // 8kb max buffer - shouldn't be more than 80 chars via pop3...
1031 if(trim($buf) == '.') {
1032 $GLOBALS['log']->debug("*** GOT '.'");
1036 // format is [msgNo] [UIDL]
1037 $exUidl = explode(" ", $buf);
1038 $UIDLs[$exUidl[0]] = trim($exUidl[1]);
1041 $this->pop3_cleanUp();
1047 * Special handler for POP3 boxes. Standard IMAP commands are useless.
1048 * This will fetch only partial emails for POP3 and hence needs to be call again and again based on status it returns
1050 function pop3_checkPartialEmail($synch = false) {
1051 require_once('include/utils/array_utils.php');
1052 global $current_user;
1053 global $sugar_config;
1055 $cacheDataExists = false;
1058 $cacheFilePath = clean_path("{$this->EmailCachePath}/{$this->id}/folders/MsgNOToUIDLData.php");
1059 if(file_exists($cacheFilePath)) {
1060 $cacheDataExists = true;
1061 if($fh = @fopen($cacheFilePath, "rb")) {
1063 $chunksize = 1*(1024*1024); // how many bytes per chunk
1065 $buf = fgets($fh, $chunksize); // 8kb max buffer - shouldn't be more than 80 chars via pop3...
1066 $data = $data . $buf;
1070 $diff = unserialize($data);
1071 if (!empty($diff)) {
1072 if (count($diff)> 50) {
1073 $newDiff = array_slice($diff, 50, count($diff), true);
1077 $results = array_slice(array_keys($diff), 0 ,50);
1078 $data = serialize($newDiff);
1079 if($fh = @fopen($cacheFilePath, "w")) {
1086 if (!$cacheDataExists) {
1088 $this->deletePop3Cache();
1090 $UIDLs = $this->pop3_getUIDL();
1091 if(count($UIDLs) > 0) {
1093 $cacheUIDLs = $this->pop3_getCacheUidls();
1095 // new email cache values we should deal with
1096 $diff = array_diff_assoc($UIDLs, $cacheUIDLs);
1097 $diff = $this->pop3_shiftCache($diff, $cacheUIDLs);
1098 require_once('modules/Emails/EmailUI.php');
1099 EmailUI::preflightEmailCache("{$this->EmailCachePath}/{$this->id}");
1101 if (count($diff)> 50) {
1102 $newDiff = array_slice($diff, 50, count($diff), true);
1107 $results = array_slice(array_keys($diff), 0 ,50);
1108 $data = serialize($newDiff);
1109 if($fh = @fopen($cacheFilePath, "w")) {
1114 $GLOBALS['log']->debug("*** INBOUNDEMAIL: could not open socket connection to POP3 server");
1115 return "could not open socket connection to POP3 server";
1119 // build up msgNo request
1120 if(count($diff) > 0) {
1121 // remove dirty cache entries
1123 if (isset($_REQUEST['currentCount']) && $_REQUEST['currentCount'] > -1) {
1124 $startingNo = $_REQUEST['currentCount'];
1127 $this->mailbox = 'INBOX';
1128 $this->connectMailserver();
1129 //$searchResults = array_keys($diff);
1130 //$fetchedOverviews = array();
1131 //$chunkArraySerachResults = array_chunk($searchResults, 50);
1132 $concatResults = implode(",", $results);
1133 $GLOBALS['log']->info('$$$$ '.$concatResults);
1134 $GLOBALS['log']->info("[EMAIL] Start POP3 fetch overview on mailbox [{$this->mailbox}] for user [{$current_user->user_name}] on 50 data");
1135 $fetchedOverviews = imap_fetch_overview($this->conn, $concatResults);
1136 $GLOBALS['log']->info("[EMAIL] End POP3 fetch overview on mailbox [{$this->mailbox}] for user [{$current_user->user_name}] on "
1137 . sizeof($fetchedOverviews) . " data");
1139 // clean up cache entry
1140 foreach($fetchedOverviews as $k => $overview) {
1141 $overview->message_id = trim($diff[$overview->msgno]);
1142 $fetchedOverviews[$k] = $overview;
1145 $GLOBALS['log']->info("[EMAIL] Start updating overview cache for pop3 mailbox [{$this->mailbox}] for user [{$current_user->user_name}]");
1146 $this->updateOverviewCacheFile($fetchedOverviews);
1147 $GLOBALS['log']->info("[EMAIL] Start updating overview cache for pop3 mailbox [{$this->mailbox}] for user [{$current_user->user_name}]");
1148 return array('status' => "In Progress", 'mbox' => $this->mailbox, 'count'=> (count($results) + $startingNo), 'totalcount' => count($diff), 'ieid' => $this->id);
1150 unlink($cacheFilePath);
1151 return array('status' => "done");
1156 * Special handler for POP3 boxes. Standard IMAP commands are useless.
1158 function pop3_checkEmail() {
1159 if($this->pop3_open()) {
1161 $this->pop3_sendCommand("USER", $this->email_user);
1162 $this->pop3_sendCommand("PASS", $this->email_password);
1165 $this->pop3_sendCommand("UIDL", '', false); // leave socket buffer alone until the while()
1166 fgets($this->pop3socket, 1024); // handle "OK+";
1171 if(is_resource($this->pop3socket)) {
1172 while(!feof($this->pop3socket)) {
1173 $buf = fgets($this->pop3socket, 1024); // 8kb max buffer - shouldn't be more than 80 chars via pop3...
1176 if(trim($buf) == '.') {
1177 $GLOBALS['log']->debug("*** GOT '.'");
1181 // format is [msgNo] [UIDL]
1182 $exUidl = explode(" ", $buf);
1183 $UIDLs[$exUidl[0]] = trim($exUidl[1]);
1187 $this->pop3_cleanUp();
1190 $cacheUIDLs = $this->pop3_getCacheUidls();
1191 // _pp($UIDLs);_pp($cacheUIDLs);
1193 // new email cache values we should deal with
1194 $diff = array_diff_assoc($UIDLs, $cacheUIDLs);
1196 // remove dirty cache entries
1197 $diff = $this->pop3_shiftCache($diff, $cacheUIDLs);
1199 // build up msgNo request
1201 $this->mailbox = 'INBOX';
1202 $this->connectMailserver();
1203 $searchResults = array_keys($diff);
1204 $concatResults = implode(",", $searchResults);
1205 $fetchedOverviews = imap_fetch_overview($this->conn, $concatResults);
1207 // clean up cache entry
1208 foreach($fetchedOverviews as $k => $overview) {
1209 $overview->message_id = trim($diff[$overview->msgno]);
1210 $fetchedOverviews[$k] = $overview;
1213 $this->updateOverviewCacheFile($fetchedOverviews);
1216 $GLOBALS['log']->debug("*** INBOUNDEMAIL: could not open socket connection to POP3 server");
1222 * Iterates through msgno and message_id to remove dirty cache entries
1225 function pop3_shiftCache($diff, $cacheUIDLs) {
1228 $newArray = array();
1229 foreach($diff as $msgNo => $msgId) {
1230 if (in_array($msgId, $cacheUIDLs)) {
1231 $q1 = "UPDATE email_cache SET imap_uid = {$msgNo}, msgno = {$msgNo} WHERE ie_id = '{$this->id}' AND message_id = '{$msgId}'";
1232 $this->db->query($q1);
1234 $newArray[$msgNo] = $msgId;
1239 foreach($diff as $msgNo => $msgId) {
1240 if(!empty($msgNos)) {
1243 if(!empty($msgIds)) {
1248 $msgIds .= "'{$msgId}'";
1251 if(!empty($msgNos)) {
1252 $q1 = "DELETE FROM email_cache WHERE ie_id = '{$this->id}' AND msgno IN ({$msgNos})";
1253 $this->db->query($q1);
1255 if(!empty($msgIds)) {
1256 $q2 = "DELETE FROM email_cache WHERE ie_id = '{$this->id}' AND message_id IN ({$msgIds})";
1257 $this->db->query($q2);
1263 * retrieves cached uidl values.
1264 * When dealing with POP3 accounts, the message_id column in email_cache will contain the UIDL.
1267 function pop3_getCacheUidls() {
1268 $q = "SELECT msgno, message_id FROM email_cache WHERE ie_id = '{$this->id}'";
1269 $r = $this->db->query($q);
1272 while($a = $this->db->fetchByAssoc($r)) {
1273 $ret[$a['msgno']] = $a['message_id'];
1280 * This function is used by cron job for group mailbox without group folder
1281 * @param string $msgno for pop
1282 * @param string $uid for imap
1284 function getMessagesInEmailCache($msgno, $uid) {
1285 $fetchedOverviews = array();
1286 if ($this->isPop3Protocol()) {
1287 $fetchedOverviews = imap_fetch_overview($this->conn, $msgno);
1288 foreach($fetchedOverviews as $k => $overview) {
1289 $overview->message_id = $uid;
1290 $fetchedOverviews[$k] = $overview;
1293 $fetchedOverviews = imap_fetch_overview($this->conn, $uid, FT_UID);
1295 $this->updateOverviewCacheFile($fetchedOverviews);
1300 * Checks email (local caching too) for one mailbox
1301 * @param string $mailbox IMAP Mailbox path
1302 * @param bool $prefetch Flag to prefetch email body on check
1304 function checkEmailOneMailbox($mailbox, $prefetch=true, $synchronize=false) {
1305 global $sugar_config;
1306 global $current_user;
1307 global $app_strings;
1311 $GLOBALS['log']->info("INBOUNDEMAIL: checking mailbox [ {$mailbox} ]");
1312 $this->mailbox = $mailbox;
1313 $this->connectMailserver();
1316 $shouldProcessRules = true;
1318 $timestamp = $this->getCacheTimestamp($mailbox);
1320 if($timestamp > 0) {
1321 $checkTime = date('r', $timestamp);
1324 /* first time through, process ALL emails */
1325 if(empty($checkTime) || $synchronize) {
1326 // do not process rules for the first time or sunchronize
1327 $shouldProcessRules = false;
1328 $criteria = "ALL UNDELETED";
1329 $prefetch = false; // do NOT prefetch emails on a brand new account - timeouts happen.
1330 $GLOBALS['log']->info("INBOUNDEMAIL: new account detected - not prefetching email bodies.");
1332 $criteria = "SINCE \"{$checkTime}\" UNDELETED"; // not using UNSEEN
1334 $this->setCacheTimestamp($mailbox);
1335 $GLOBALS['log']->info("[EMAIL] Performing IMAP search using criteria [{$criteria}] on mailbox [{$mailbox}] for user [{$current_user->user_name}]");
1336 $searchResults = imap_search($this->conn, $criteria, SE_UID);
1337 $GLOBALS['log']->info("[EMAIL] Done IMAP search on mailbox [{$mailbox}] for user [{$current_user->user_name}]. Result count = ".count($searchResults));
1339 if(!empty($searchResults)) {
1341 $concatResults = implode(",", $searchResults);
1342 $GLOBALS['log']->info("[EMAIL] Start IMAP fetch overview on mailbox [{$mailbox}] for user [{$current_user->user_name}]");
1343 $fetchedOverview = imap_fetch_overview($this->conn, $concatResults, FT_UID);
1344 $GLOBALS['log']->info("[EMAIL] Done IMAP fetch overview on mailbox [{$mailbox}] for user [{$current_user->user_name}]");
1346 $GLOBALS['log']->info("[EMAIL] Start updating overview cache for mailbox [{$mailbox}] for user [{$current_user->user_name}]");
1347 $this->updateOverviewCacheFile($fetchedOverview);
1348 $GLOBALS['log']->info("[EMAIL] Done updating overview cache for mailbox [{$mailbox}] for user [{$current_user->user_name}]");
1351 if($prefetch == true) {
1352 $GLOBALS['log']->info("[EMAIL] Start fetching emails for mailbox [{$mailbox}] for user [{$current_user->user_name}]");
1353 if(!$this->fetchCheckedEmails($fetchedOverview))
1357 $GLOBALS['log']->info("[EMAIL] Done fetching emails for mailbox [{$mailbox}] for user [{$current_user->user_name}]");
1360 $GLOBALS['log']->info("INBOUNDEMAIL: no results for mailbox [ {$mailbox} ]");
1365 * To handle the use case where an external client is also connected, deleting emails, we need to clear our
1366 * local cache of all emails with the "DELETED" flag
1368 $criteria = 'DELETED';
1369 $criteria .= (!empty($checkTime)) ? " SINCE \"{$checkTime}\"" : "";
1370 $GLOBALS['log']->info("INBOUNDEMAIL: checking for deleted emails using [ {$criteria} ]");
1372 $trashFolder = $this->get_stored_options("trashFolder");
1373 if (empty($trashFolder)) {
1374 $trashFolder = "INBOX.Trash";
1377 if($this->mailbox != $trashFolder) {
1378 $searchResults = imap_search($this->conn, $criteria, SE_UID);
1379 if(!empty($searchResults)) {
1380 $uids = implode($app_strings['LBL_EMAIL_DELIMITER'], $searchResults);
1381 $GLOBALS['log']->info("INBOUNDEMAIL: removing UIDs found deleted [ {$uids} ]");
1382 $this->getOverviewsFromCacheFile($uids, $mailbox, true);
1389 * Checks email (local caching too) for one mailbox
1390 * @param string $mailbox IMAP Mailbox path
1391 * @param bool $prefetch Flag to prefetch email body on check
1393 function checkEmailOneMailboxPartial($mailbox, $prefetch=true, $synchronize=false, $start = 0, $max = -1) {
1394 global $sugar_config;
1395 global $current_user;
1396 global $app_strings;
1398 $GLOBALS['log']->info("INBOUNDEMAIL: checking mailbox [ {$mailbox} ]");
1399 $this->mailbox = $mailbox;
1400 $this->connectMailserver();
1403 $shouldProcessRules = true;
1405 $timestamp = $this->getCacheTimestamp($mailbox);
1407 if($timestamp > 0) {
1408 $checkTime = date('r', $timestamp);
1411 /* first time through, process ALL emails */
1412 if(empty($checkTime) || $synchronize) {
1413 // do not process rules for the first time or sunchronize
1414 $shouldProcessRules = false;
1415 $criteria = "ALL UNDELETED";
1416 $prefetch = false; // do NOT prefetch emails on a brand new account - timeouts happen.
1417 $GLOBALS['log']->info("INBOUNDEMAIL: new account detected - not prefetching email bodies.");
1419 $criteria = "SINCE \"{$checkTime}\" UNDELETED"; // not using UNSEEN
1421 $this->setCacheTimestamp($mailbox);
1422 $GLOBALS['log']->info("[EMAIL] Performing IMAP search using criteria [{$criteria}] on mailbox [{$mailbox}] for user [{$current_user->user_name}]");
1423 $searchResults = $this->getCachedIMAPSearch($criteria);
1425 if(!empty($searchResults)) {
1427 $total = sizeof($searchResults);
1428 $searchResults = array_slice($searchResults, $start, $max);
1430 $GLOBALS['log']->info("INBOUNDEMAIL: there are $total messages in [{$mailbox}], we are on $start");
1431 $GLOBALS['log']->info("INBOUNDEMAIL: getting the next " . sizeof($searchResults) . " messages");
1432 $concatResults = implode(",", $searchResults);
1433 $GLOBALS['log']->info("INBOUNDEMAIL: Start IMAP fetch overview on mailbox [{$mailbox}] for user [{$current_user->user_name}]");
1434 $fetchedOverview = imap_fetch_overview($this->conn, $concatResults, FT_UID);
1435 $GLOBALS['log']->info("INBOUNDEMAIL: Done IMAP fetch overview on mailbox [{$mailbox}] for user [{$current_user->user_name}]");
1437 $GLOBALS['log']->info("INBOUNDEMAIL: Start updating overview cache for mailbox [{$mailbox}] for user [{$current_user->user_name}]");
1438 $this->updateOverviewCacheFile($fetchedOverview);
1439 $GLOBALS['log']->info("INBOUNDEMAIL: Done updating overview cache for mailbox [{$mailbox}] for user [{$current_user->user_name}]");
1442 if($prefetch == true) {
1443 $GLOBALS['log']->info("INBOUNDEMAIL: Start fetching emails for mailbox [{$mailbox}] for user [{$current_user->user_name}]");
1444 $this->fetchCheckedEmails($fetchedOverview);
1445 $GLOBALS['log']->info("INBOUNDEMAIL: Done fetching emails for mailbox [{$mailbox}] for user [{$current_user->user_name}]");
1447 $status = ($total > $start + sizeof($searchResults)) ? 'continue' : 'done';
1448 $ret = array('status' => $status, 'count' => $start + sizeof($searchResults), 'mbox' => $mailbox, 'totalcount' => $total);
1449 $GLOBALS['log']->info("INBOUNDEMAIL: $status : Downloaded " . $start + sizeof($searchResults) . "messages of $total");
1452 $GLOBALS['log']->info("INBOUNDEMAIL: no results for mailbox [ {$mailbox} ]");
1453 $ret = array('status' =>'done');
1456 if ($ret['status'] == 'done') {
1457 //Remove the cached search if we are done with this mailbox
1458 $cacheFilePath = clean_path("{$this->EmailCachePath}/{$this->id}/folders/SearchData.php");
1459 unlink($cacheFilePath);
1461 * To handle the use case where an external client is also connected, deleting emails, we need to clear our
1462 * local cache of all emails with the "DELETED" flag
1464 $criteria = 'DELETED';
1465 $criteria .= (!empty($checkTime)) ? " SINCE \"{$checkTime}\"" : "";
1466 $GLOBALS['log']->info("INBOUNDEMAIL: checking for deleted emails using [ {$criteria} ]");
1468 $trashFolder = $this->get_stored_options("trashFolder");
1469 if (empty($trashFolder)) {
1470 $trashFolder = "INBOX.Trash";
1473 if($this->mailbox != $trashFolder) {
1474 $searchResults = imap_search($this->conn, $criteria, SE_UID);
1475 if(!empty($searchResults)) {
1476 $uids = implode($app_strings['LBL_EMAIL_DELIMITER'], $searchResults);
1477 $GLOBALS['log']->info("INBOUNDEMAIL: removing UIDs found deleted [ {$uids} ]");
1478 $this->getOverviewsFromCacheFile($uids, $mailbox, true);
1485 function getCachedIMAPSearch($criteria) {
1486 global $current_user;
1487 global $sugar_config;
1489 $cacheDataExists = false;
1492 $cacheFolderPath = clean_path("{$this->EmailCachePath}/{$this->id}/folders");
1493 if (!file_exists($cacheFolderPath)) {
1494 mkdir_recursive($cacheFolderPath);
1496 $cacheFilePath = $cacheFolderPath . '/SearchData.php';
1497 $GLOBALS['log']->info("INBOUNDEMAIL: Cache path is $cacheFilePath");
1498 if(file_exists($cacheFilePath)) {
1499 $cacheDataExists = true;
1500 if($fh = @fopen($cacheFilePath, "rb")) {
1502 $chunksize = 1*(1024*1024); // how many bytes per chunk
1504 $buf = fgets($fh, $chunksize); // 8kb max buffer - shouldn't be more than 80 chars via pop3...
1505 $data = $data . $buf;
1509 $results = unserialize($data);
1512 if (!$cacheDataExists) {
1513 $searchResults = imap_search($this->conn, $criteria, SE_UID);
1514 if(count($searchResults) > 0) {
1515 $results = $searchResults;
1516 $data = serialize($searchResults);
1517 if($fh = @fopen($cacheFilePath, "w")) {
1526 function checkEmailIMAPPartial($prefetch=true, $synch = false) {
1527 $GLOBALS['log']->info("*****************INBOUNDEMAIL: at IMAP check partial");
1528 global $sugar_config;
1529 $result = $this->connectMailserver();
1530 if ($result == 'false')
1533 'status' => 'error',
1534 'message' => 'Email server is down'
1537 $mailboxes = $this->getMailboxes(true);
1538 if (!in_array('INBOX', $mailboxes)) {
1539 $mailboxes[] = 'INBOX';
1542 if (isset($_REQUEST['mbox']) && !empty($_REQUEST['mbox']) && isset($_REQUEST['currentCount'])) {
1543 $GLOBALS['log']->info("INBOUNDEMAIL: Picking up from where we left off");
1544 $mbox = $_REQUEST['mbox'];
1545 $count = $_REQUEST['currentCount'];
1548 $GLOBALS['log']->info("INBOUNDEMAIL: Cleaning out the cache");
1549 $this->cleanOutCache();
1551 $mbox = $mailboxes[0];
1554 $GLOBALS['log']->info("INBOUNDEMAIL:found " . sizeof($mailboxes) . " Mailboxes");
1555 $index = array_search($mbox, $mailboxes) + 1;
1556 $ret = $this->checkEmailOneMailboxPartial($mbox, $prefetch, $synch, $count, 100);
1557 while($ret['status'] == 'done' && $index < sizeof($mailboxes)) {
1558 if ($ret['count'] > 100) {
1559 $ret['mbox'] = $mailboxes[$index];
1560 $ret['status'] = 'continue';
1563 $GLOBALS['log']->info("INBOUNDEMAIL: checking account [ $index => $mbox : $count]");
1564 $mbox = $mailboxes[$index];
1565 $ret = $this->checkEmailOneMailboxPartial($mbox, $prefetch, $synch, 0, 100);
1572 function checkEmail2_meta() {
1573 global $sugar_config;
1575 $this->connectMailserver();
1576 $mailboxes = $this->getMailboxes(true);
1577 $mailboxes[] = 'INBOX';
1580 $GLOBALS['log']->info("INBOUNDEMAIL: checking account [ {$this->name} ]");
1582 $mailboxes_meta = array();
1583 foreach($mailboxes as $mailbox) {
1584 $mailboxes_meta[$mailbox] = $this->getMailboxProcessCount($mailbox);
1588 $ret['mailboxes'] = $mailboxes_meta;
1590 foreach($mailboxes_meta as $count) {
1591 $ret['processCount'] += $count;
1596 function getMailboxProcessCount($mailbox) {
1597 global $sugar_config;
1599 $GLOBALS['log']->info("INBOUNDEMAIL: checking mailbox [ {$mailbox} ]");
1600 $this->mailbox = $mailbox;
1601 $this->connectMailserver();
1603 $timestamp = $this->getCacheTimestamp($mailbox);
1606 if($timestamp > 0) {
1607 $checkTime = date('r', $timestamp);
1610 /* first time through, process ALL emails */
1611 if(empty($checkTime)) {
1612 $criteria = "ALL UNDELETED";
1613 $prefetch = false; // do NOT prefetch emails on a brand new account - timeouts happen.
1614 $GLOBALS['log']->info("INBOUNDEMAIL: new account detected - not prefetching email bodies.");
1616 $criteria = "SINCE \"{$checkTime}\" UNDELETED"; // not using UNSEEN
1619 $GLOBALS['log']->info("INBOUNDEMAIL: using [ {$criteria} ]");
1620 $searchResults = imap_search($this->conn, $criteria, SE_UID);
1622 if(!empty($searchResults)) {
1623 $concatResults = implode(",", $searchResults);
1625 $GLOBALS['log']->info("INBOUNDEMAIL: no results for mailbox [ {$mailbox} ]");
1628 if(empty($searchResults)) {
1632 return count($searchResults);
1638 function checkEmail($prefetch=true, $synch = false) {
1639 global $sugar_config;
1641 if($this->protocol == 'pop3') {
1642 $this->pop3_checkEmail();
1644 $this->connectMailserver();
1645 $mailboxes = $this->getMailboxes(true);
1648 $GLOBALS['log']->info("INBOUNDEMAIL: checking account [ {$this->name} ]");
1650 foreach($mailboxes as $mailbox) {
1651 $this->checkEmailOneMailbox($mailbox, $prefetch, $synch);
1657 * full synchronization
1659 function syncEmail() {
1660 global $sugar_config;
1661 global $current_user;
1663 $showFolders = unserialize(base64_decode($current_user->getPreference('showFolders', 'Emails')));
1665 if(empty($showFolders)) {
1666 $showFolders = array();
1669 $email = new Email();
1670 $email->email2init();
1672 // personal accounts
1673 if($current_user->hasPersonalEmail()) {
1674 $personals = $this->retrieveByGroupId($current_user->id);
1676 foreach($personals as $personalAccount) {
1677 if(in_array($personalAccount->id, $showFolders)) {
1678 $personalAccount->email = $email;
1679 if ($personalAccount->isPop3Protocol()) {
1680 $personalAccount->deletePop3Cache();
1683 $personalAccount->cleanOutCache();
1684 $personalAccount->connectMailserver();
1685 $mailboxes = $personalAccount->getMailboxes(true);
1686 $mailboxes[] = 'INBOX';
1689 $GLOBALS['log']->info("[EMAIL] Start checking account [{$personalAccount->name}] for user [{$current_user->user_name}]");
1691 foreach($mailboxes as $mailbox) {
1692 $GLOBALS['log']->info("[EMAIL] Start checking mailbox [{$mailbox}] of account [{$personalAccount->name}] for user [{$current_user->user_name}]");
1693 $personalAccount->checkEmailOneMailbox($mailbox, false, true);
1694 $GLOBALS['log']->info("[EMAIL] Done checking mailbox [{$mailbox}] of account [{$personalAccount->name}] for user [{$current_user->user_name}]");
1696 $GLOBALS['log']->info("[EMAIL] Done checking account [{$personalAccount->name}] for user [{$current_user->user_name}]");
1702 $beans = $this->retrieveAllByGroupId($current_user->id, false);
1703 foreach($beans as $k => $groupAccount) {
1704 if(in_array($groupAccount->id, $showFolders)) {
1705 $groupAccount->email = $email;
1706 $groupAccount->cleanOutCache();
1707 $groupAccount->connectMailserver();
1708 $mailboxes = $groupAccount->getMailboxes(true);
1709 $mailboxes[] = 'INBOX';
1712 $GLOBALS['log']->info("INBOUNDEMAIL: checking account [ {$groupAccount->name} ]");
1714 foreach($mailboxes as $mailbox) {
1715 $groupAccount->checkEmailOneMailbox($mailbox, false, true);
1723 * Deletes cached messages when moving from folder to folder
1724 * @param string $uids
1725 * @param string $fromFolder
1726 * @param string $toFolder
1728 function deleteCachedMessages($uids, $fromFolder) {
1729 global $sugar_config;
1731 if(!isset($this->email) && !isset($this->email->et)) {
1732 $this->email = new Email();
1733 $this->email->email2init();
1736 $uids = $this->email->et->_cleanUIDList($uids);
1738 foreach($uids as $uid) {
1739 $file = "{$this->EmailCachePath}/{$this->id}/messages/{$fromFolder}{$uid}.php";
1741 if(file_exists($file)) {
1742 if(!unlink($file)) {
1743 $GLOBALS['log']->debug("INBOUNDEMAIL: Could not delete [ {$file} ]");
1750 * similar to imap_fetch_overview, but it gets overviews from a local cache
1752 * @param string $uids UIDs in comma-delimited format
1753 * @param string $mailbox The mailbox in focus, will default to $this->mailbox
1754 * @param bool $remove Default false
1757 function getOverviewsFromCacheFile($uids, $mailbox='', $remove=false) {
1758 global $app_strings;
1759 if(!isset($this->email) && !isset($this->email->et)) {
1760 $this->email = new Email();
1761 $this->email->email2init();
1764 $uids = $this->email->et->_cleanUIDList($uids, true);
1766 // load current cache file
1767 $mailbox = empty($mailbox) ? $this->mailbox : $mailbox;
1768 $cacheValue = $this->getCacheValue($mailbox);
1772 $exUids = explode($app_strings['LBL_EMAIL_DELIMITER'], $uids);
1773 foreach($exUids as $k => $uid) {
1774 $exUids[$k] = trim($uid);
1777 // fill $ret will requested $uids
1778 foreach($cacheValue['retArr'] as $k => $overview) {
1779 if(in_array($overview->imap_uid, $exUids)) {
1784 // remove requested $uids from current cache file (move_mail() type action)
1786 $this->setCacheValue($mailbox, array(), array(), $ret);
1792 * merges new info with the saved cached file
1793 * @param array $array Array of email Overviews
1794 * @param string $type 'append' or 'remove'
1795 * @param string $mailbox Target mailbox if not current assigned
1797 function updateOverviewCacheFile($array, $type='append', $mailbox='') {
1798 $mailbox = empty($mailbox) ? $this->mailbox : $mailbox;
1800 $cacheValue = $this->getCacheValue($mailbox);
1801 $uids = $cacheValue['uids'];
1803 $updateRows = array();
1804 $insertRows = array();
1805 $removeRows = array();
1808 if($type == 'append') { // append
1809 /* we are adding overviews to the cache file */
1810 foreach($array as $overview) {
1811 if(isset($overview->uid)) {
1812 $overview->imap_uid = $overview->uid; // coming from imap_fetch_overview() call
1815 if(!in_array($overview->imap_uid, $uids)) {
1816 $insertRows[] = $overview;
1820 $updatedCacheOverviews = array();
1821 // compare against generated list
1822 /* we are removing overviews from the cache file */
1823 foreach($cacheValue['retArr'] as $cacheOverview) {
1824 if(!in_array($cacheOverview->imap_uid, $uids)) {
1825 $insertRows[] = $cacheOverview;
1827 $removeRows[] = $cacheOverview;
1831 $cacheValue['retArr'] = $updatedCacheOverviews;
1834 $this->setCacheValue($mailbox, $insertRows, $updateRows, $removeRows);
1838 * Check email prefetches email bodies for quicker display
1839 * @param array array of fetched overviews
1841 function fetchCheckedEmails($fetchedOverviews) {
1842 global $sugar_config;
1844 if(is_array($fetchedOverviews) && !empty($fetchedOverviews)) {
1845 foreach($fetchedOverviews as $overview) {
1846 if($overview->size < 10000) {
1848 $uid = $overview->imap_uid;
1851 $file = "{$this->mailbox}{$uid}.php";
1852 $cacheFile = clean_path("{$this->EmailCachePath}/{$this->id}/messages/{$file}");
1854 if(!file_exists($cacheFile)) {
1855 $GLOBALS['log']->info("INBOUNDEMAIL: Prefetching email [ {$file} ]");
1856 $this->setEmailForDisplay($uid);
1857 $out = $this->displayOneEmail($uid, $this->mailbox);
1858 $this->email->et->writeCacheFile('out', $out, $this->id, 'messages', "{$this->mailbox}{$uid}.php");
1860 $GLOBALS['log']->debug("INBOUNDEMAIL: Trying to prefetch an email we already fetched! [ {$cacheFile} ]");
1863 $GLOBALS['log']->debug("*** INBOUNDEMAIL: prefetch has a message with no UID");
1867 $GLOBALS['log']->debug("INBOUNDEMAIL: skipping email prefetch - size too large [ {$overview->size} ]");
1875 * Sets flags on emails. Assumes that connection is live, correct folder is
1877 * @param string $uids Sequence of UIDs, comma separated
1878 * @param string $type Flag to mark
1880 function markEmails($uids, $type) {
1883 $result = imap_clearflag_full($this->conn, $uids, '\\SEEN', ST_UID);
1886 $result = imap_setflag_full($this->conn, $uids, '\\SEEN', ST_UID);
1889 $result = imap_setflag_full($this->conn, $uids, '\\FLAGGED', ST_UID);
1892 $result = imap_clearflag_full($this->conn, $uids, '\\FLAGGED', ST_UID);
1895 $result = imap_setflag_full($this->conn, $uids, '\\Answered', ST_UID);
1899 //// END EMAIL 2.0 SPECIFIC
1900 ///////////////////////////////////////////////////////////////////////////
1904 ///////////////////////////////////////////////////////////////////////////
1905 //// SERVER MANIPULATION METHODS
1907 * Deletes the specified folder
1908 * @param string $mbox "::" delimited IMAP mailbox path, ie, INBOX.saved.stuff
1911 function deleteFolder($mbox) {
1912 $returnArray = array();
1913 if ($this->getCacheCount($mbox) > 0) {
1914 $returnArray['status'] = false;
1915 $returnArray['errorMessage'] = "Can not delete {$mbox} as it has emails.";
1916 return $returnArray;
1918 $connectString = $this->getConnectString('', $mbox);
1919 //Remove Folder cache
1920 global $sugar_config;
1921 unlink("{$this->EmailCachePath}/{$this->id}/folders/folders.php");
1923 if(imap_unsubscribe($this->conn, imap_utf7_encode($connectString))) {
1924 if(imap_deletemailbox($this->conn, $connectString)) {
1925 $this->mailbox = str_replace(("," . $mbox), "", $this->mailbox);
1927 $sessionFoldersString = $this->getSessionInboundFoldersString($this->server_url, $this->email_user, $this->port, $this->protocol);
1928 $sessionFoldersString = str_replace(("," . $mbox), "", $sessionFoldersString);
1929 $this->setSessionInboundFoldersString($this->server_url, $this->email_user, $this->port, $this->protocol, $sessionFoldersString);
1930 $returnArray['status'] = true;
1931 return $returnArray;
1933 $GLOBALS['log']->error("*** ERROR: EMAIL2.0 - could not delete IMAP mailbox with path: [ {$connectString} ]");
1934 $returnArray['status'] = false;
1935 $returnArray['errorMessage'] = "NOOP: could not delete folder: {$connectString}";
1936 return $returnArray;
1940 $GLOBALS['log']->error("*** ERROR: EMAIL2.0 - could not unsubscribe from folder, {$connectString} before deletion.");
1941 $returnArray['status'] = false;
1942 $returnArray['errorMessage'] = "NOOP: could not unsubscribe from folder, {$connectString} before deletion.";
1943 return $returnArray;
1949 * @param string $name Name of new IMAP mailbox
1950 * @param string $mbox "::" delimited IMAP mailbox path, ie, INBOX.saved.stuff
1951 * @return bool True on success
1953 function saveNewFolder($name, $mbox) {
1954 global $sugar_config;
1955 //Remove Folder cache
1956 global $sugar_config;
1957 //unlink("{$this->EmailCachePath}/{$this->id}/folders/folders.php");
1959 //$mboxImap = $this->getImapMboxFromSugarProprietary($mbox);
1960 $delimiter = $this->get_stored_options('folderDelimiter');
1965 $newFolder = $mbox . $delimiter . $name;
1966 $mbox .= $delimiter.str_replace($delimiter, "_", $name);
1967 $connectString = $this->getConnectString('', $mbox);
1969 if(imap_createmailbox($this->conn, imap_utf7_encode($connectString))) {
1970 imap_subscribe($this->conn, imap_utf7_encode($connectString));
1971 $status = imap_status($this->conn, str_replace("{$delimiter}{$name}","",$connectString), SA_ALL);
1972 $this->mailbox = $this->mailbox . "," . $newFolder;
1974 $sessionFoldersString = $this->getSessionInboundFoldersString($this->server_url, $this->email_user, $this->port, $this->protocol);
1975 $sessionFoldersString = $sessionFoldersString . "," . $newFolder;
1976 $this->setSessionInboundFoldersString($this->server_url, $this->email_user, $this->port, $this->protocol, $sessionFoldersString);
1978 echo json_encode($status);
1981 echo "NOOP: could not create folder";
1982 $GLOBALS['log']->error("*** ERROR: EMAIL2.0 - could not create IMAP mailbox with path: [ {$connectString} ]");
1989 * Constructs an IMAP c-client compatible folder path from Sugar proprietary
1990 * @param string $mbox "::" delimited IMAP mailbox path, ie, INBOX.saved.stuff
1993 function getImapMboxFromSugarProprietary($mbox) {
1994 $exMbox = explode("::", $mbox);
1998 for($i=2; $i<count($exMbox); $i++) {
1999 if(!empty($mboxImap)) {
2002 $mboxImap .= $exMbox[$i];
2009 * Searches IMAP (and POP3?) accounts/folders for emails with qualifying criteria
2011 function search($ieId, $subject='', $from='', $to='', $body='', $dateFrom='', $dateTo='') {
2012 global $current_user;
2013 global $app_strings;
2017 $bean = new InboundEmail();
2018 $bean->retrieve($ieId);
2020 //$beans = $this->retrieveAllByGroupId($current_user->id, true);
2022 $subject = urldecode($subject);
2025 $criteria .= (!empty($subject)) ? 'SUBJECT '.from_html($subject).'' : "";
2026 $criteria .= (!empty($from)) ? ' FROM "'.$from.'"' : "";
2027 $criteria .= (!empty($to)) ? ' FROM "'.$to.'"' : "";
2028 $criteria .= (!empty($body)) ? ' TEXT "'.$body.'"' : "";
2029 $criteria .= (!empty($dateFrom)) ? ' SINCE "'.$timedate->fromString($dateFrom)->format('d-M-Y').'"' : "";
2030 $criteria .= (!empty($dateTo)) ? ' BEFORE "'.$timedate->fromString($dateTo)->format('d-M-Y').'"' : "";
2031 //$criteria .= (!empty($from)) ? ' FROM "'.$from.'"' : "";
2033 $showFolders = unserialize(base64_decode($current_user->getPreference('showFolders', 'Emails')));
2037 foreach($beans as $bean) {
2038 if(!in_array($bean->id, $showFolders)) {
2042 $GLOBALS['log']->info("*** INBOUNDEMAIL: searching [ {$bean->name} ] for [ {$subject}{$from}{$to}{$body}{$dateFrom}{$dateTo} ]");
2043 $group = (!$bean->is_personal) ? 'group.' : '';
2044 $bean->connectMailServer();
2045 $mailboxes = $bean->getMailboxes(true);
2046 if (!in_array('INBOX', $mailboxes)) {
2047 $mailboxes[] = 'INBOX';
2051 foreach($mailboxes as $mbox) {
2052 $bean->mailbox = $mbox;
2053 $searchOverviews = array();
2054 if ($bean->protocol == 'pop3') {
2055 $pop3Criteria = "SELECT * FROM email_cache WHERE ie_id = '{$bean->id}' AND mbox = '{$mbox}'";
2056 $pop3Criteria .= (!empty($subject)) ? ' AND subject like "%'.$bean->db->quote($subject).'%"' : "";
2057 $pop3Criteria .= (!empty($from)) ? ' AND fromaddr like "%'.$from.'%"' : "";
2058 $pop3Criteria .= (!empty($to)) ? ' AND toaddr like "%'.$to.'%"' : "";
2059 $pop3Criteria .= (!empty($dateFrom)) ? ' AND senddate > "'.$dateFrom.'"' : "";
2060 $pop3Criteria .= (!empty($dateTo)) ? ' AND senddate < "'.$dateTo.'"' : "";
2061 $GLOBALS['log']->info("*** INBOUNDEMAIL: searching [ {$mbox} ] using criteria [ {$pop3Criteria} ]");
2063 $r = $bean->db->query($pop3Criteria);
2064 while($a = $bean->db->fetchByAssoc($r)) {
2065 $overview = new Overview();
2067 foreach($a as $k => $v) {
2071 $overview->imap_uid = $v;
2072 $overview->uid = $a['message_id'];
2075 $overview->to = from_html($v);
2079 $overview->from = from_html($v);
2083 $overview->size = $v;
2087 $overview->date = $timedate->fromString($v)->format('r');
2091 $overview->$k = from_html($v);
2095 $searchOverviews[] = $overview;
2098 $bean->connectMailServer();
2099 $searchResult = imap_search($bean->conn, $criteria, SE_UID);
2100 if (!empty($searchResult)) {
2101 $searchOverviews = imap_fetch_overview($bean->conn, implode(',', $searchResult), FT_UID);
2104 $numHits = count($searchOverviews);
2107 $totalHits = $totalHits + $numHits;
2108 $ret = $bean->sortFetchedOverview($searchOverviews, 'date', 'desc', true);
2109 $mbox = "{$bean->id}.SEARCH";
2110 $out = array_merge($out, $bean->displayFetchedSortedListXML($ret, $mbox, false));
2115 $metadata = array();
2116 $metadata['mbox'] = $app_strings['LBL_EMAIL_SEARCH_RESULTS_TITLE'];
2117 $metadata['ieId'] = $this->id;
2118 $metadata['name'] = $this->name;
2119 $metadata['unreadChecked'] = ($current_user->getPreference('showUnreadOnly', 'Emails') == 1) ? 'CHECKED' : '';
2120 $metadata['out'] = $out;
2126 * repairs the encrypted password for a given I-E account
2127 * @return bool True on success
2129 function repairAccount() {
2131 for($i=0; $i<3; $i++) {
2132 if($i != 0) { // decode is performed on retrieve already
2133 $this->email_password = blowfishDecode(blowfishGetKey('InboundEmail'), $this->email_password);
2136 if($this->connectMailserver() == 'true') {
2137 $this->save(); // save decoded password (is encoded on save())
2146 * soft deletes a User's personal inbox
2147 * @param string id I-E id
2148 * @param string user_name User name of User in focus, NOT current_user
2149 * @return bool True on success
2151 function deletePersonalEmailAccount($id, $user_name) {
2152 $q = "SELECT ie.id FROM inbound_email ie LEFT JOIN users u ON ie.group_id = u.id WHERE u.user_name = '{$user_name}'";
2153 $r = $this->db->query($q, true);
2155 while($a = $this->db->fetchByAssoc($r)) {
2156 if(!empty($a) && $a['id'] == $id) {
2157 $this->retrieve($id);
2166 function getTeamSetIdForTeams($teamIds) {
2167 if(!is_array($teamIds)){
2168 $teamIds = array($teamIds);
2170 $teamSet = new TeamSet();
2171 $team_set_id = $teamSet->addTeams($teamIds);
2172 return $team_set_id;
2176 * Saves Personal Inbox settings for Users
2177 * @param string userId ID of user to assign all emails for this account
2178 * @param strings userName Name of account, for Sugar purposes
2179 * @param bool forceSave Default true. Flag to save errored settings.
2180 * @return boolean true on success, false on fail
2182 function savePersonalEmailAccount($userId = '', $userName = '', $forceSave=true) {
2184 $accountExists = false;
2185 if(isset($_REQUEST['ie_id']) && !empty($_REQUEST['ie_id'])) {
2186 $this->retrieve($_REQUEST['ie_id']);
2187 $accountExists = true;
2189 $ie_name = $_REQUEST['ie_name'];
2191 $this->is_personal = 1;
2192 $this->name = $ie_name;
2193 $this->group_id = $groupId;
2194 $this->status = $_REQUEST['ie_status'];
2195 $this->server_url = trim($_REQUEST['server_url']);
2196 $this->email_user = trim($_REQUEST['email_user']);
2197 if(!empty($_REQUEST['email_password'])) {
2198 $this->email_password = html_entity_decode($_REQUEST['email_password'], ENT_QUOTES);
2200 $this->port = trim($_REQUEST['port']);
2201 $this->protocol = $_REQUEST['protocol'];
2202 if ($this->protocol == "pop3") {
2203 $_REQUEST['mailbox'] = "INBOX";
2205 $this->mailbox = $_REQUEST['mailbox'];
2206 $this->mailbox_type = 'pick'; // forcing this
2209 if(isset($_REQUEST['ssl']) && $_REQUEST['ssl'] == 1) { $useSsl = true; }
2210 else $useSsl = false;
2211 $this->service = '::::::::::';
2214 $id = $this->save(); // saving here to prevent user from having to re-enter all the info in case of error
2215 $this->retrieve($id);
2218 $this->protocol = $_REQUEST['protocol']; // need to set this again since we safe the "service" string to empty explode values
2219 $opts = $this->getSessionConnectionString($this->server_url, $this->email_user, $this->port, $this->protocol);
2220 $detectedOpts = $this->findOptimumSettings($useSsl);
2222 //If $detectedOpts is empty, there was an error connecting, so clear $opts. If $opts was empty, use $detectedOpts
2223 if (empty($opts) || empty($detectedOpts) || (empty($detectedOpts['good']) && empty($detectedOpts['serial'])))
2225 $opts = $detectedOpts;
2227 $delimiter = $this->getSessionInboundDelimiterString($this->server_url, $this->email_user, $this->port, $this->protocol);
2229 if(isset($opts['serial']) && !empty($opts['serial'])) {
2230 $this->service = $opts['serial'];
2231 if(isset($_REQUEST['mark_read']) && $_REQUEST['mark_read'] == 1) {
2232 $this->delete_seen = 0;
2234 $this->delete_seen = 1;
2237 // handle stored_options serialization
2238 if(isset($_REQUEST['only_since']) && $_REQUEST['only_since'] == 1) {
2244 $focusUser = new User();
2245 $focusUser->retrieve($groupId);
2246 $mailerId = (isset($_REQUEST['outbound_email'])) ? $_REQUEST['outbound_email'] : "";
2248 $oe = new OutboundEmail();
2249 $oe->getSystemMailerSettings($focusUser, $mailerId);
2251 $stored_options = array();
2252 $stored_options['from_name'] = trim($_REQUEST['from_name']);
2253 $stored_options['from_addr'] = trim($_REQUEST['from_addr']);
2254 $stored_options['reply_to_addr'] = trim($_REQUEST['reply_to_addr']);
2256 if (!$this->isPop3Protocol()) {
2257 $stored_options['trashFolder'] = (isset($_REQUEST['trashFolder']) ? trim($_REQUEST['trashFolder']) : "");
2258 $stored_options['sentFolder'] = (isset($_REQUEST['sentFolder']) ? trim($_REQUEST['sentFolder']) : "");
2260 $stored_options['only_since'] = $onlySince;
2261 $stored_options['filter_domain'] = '';
2262 $storedOptions['folderDelimiter'] = $delimiter;
2263 $stored_options['outbound_email'] = (isset($_REQUEST['outbound_email'])) ? $_REQUEST['outbound_email'] : $oe->id;
2264 $this->stored_options = base64_encode(serialize($stored_options));
2266 $ieId = $this->save();
2268 //If this is the first personal account the user has setup mark it as default for them.
2269 $currentIECount = $this->getUserPersonalAccountCount($focusUser);
2270 if($currentIECount == 1)
2271 $this->setUsersDefaultOutboundServerId($focusUser, $ieId);
2275 // could not find opts, no save
2276 $GLOBALS['log']->debug('-----> InboundEmail could not find optimums for User: '.$ie_name);
2281 * Determines if this instance of I-E is for a Group Inbox or Personal Inbox
2283 function handleIsPersonal() {
2284 $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.'\'';
2285 $rp = $this->db->query($qp, true);
2286 $personalBox = array();
2287 while($ap = $this->db->fetchByAssoc($rp)) {
2288 $personalBox[] = array($ap['id'], $ap['user_name']);
2290 if(count($personalBox) > 0) {
2297 function getUserNameFromGroupId() {
2298 $r = $this->db->query('SELECT users.user_name FROM users WHERE deleted=0 AND id=\''.$this->group_id.'\'', true);
2299 while($a = $this->db->fetchByAssoc($r)) {
2300 return $a['user_name'];
2305 function getFoldersListForMailBox() {
2307 $foldersList = $this->getSessionInboundFoldersString($this->server_url, $this->email_user, $this->port, $this->protocol);
2308 if (empty($foldersList)) {
2309 global $mod_strings;
2310 $msg = $this->connectMailserver(true);
2311 if (strpos($msg, "successfully")) {
2312 $foldersList = $this->getSessionInboundFoldersString($this->server_url, $this->email_user, $this->port, $this->protocol);
2313 $return['status'] = true;
2314 $return['foldersList'] = $foldersList;
2315 $return['statusMessage'] = "";
2317 $return['status'] = false;
2318 $return['statusMessage'] = $msg;
2321 $return['status'] = true;
2322 $return['foldersList'] = $foldersList;
2323 $return['statusMessage'] = "";
2328 * Programatically determines best-case settings for imap_open()
2330 function findOptimumSettings($useSsl=false, $user='', $pass='', $server='', $port='', $prot='', $mailbox='') {
2331 global $mod_strings;
2332 $serviceArr = array();
2333 $returnService = array();
2334 $badService = array();
2335 $goodService = array();
2336 $errorArr = array();
2338 $retArray = array( 'good' => $goodService,
2339 'bad' => $badService,
2340 'err' => $errorArr);
2342 if(!function_exists('imap_open')) {
2343 $retArray['err'][0] = $mod_strings['ERR_NO_IMAP'];
2347 imap_errors(); // clearing error stack
2348 error_reporting(0); // turn off notices from IMAP
2350 if(isset($_REQUEST['ssl']) && $_REQUEST['ssl'] == 1) {
2354 $exServ = explode('::', $this->service);
2355 $service = '/'.$exServ[1];
2357 $nonSsl = array('both-secure' => '/notls/novalidate-cert/secure',
2358 'both' => '/notls/novalidate-cert',
2359 'nocert-secure' => '/novalidate-cert/secure',
2360 'nocert' => '/novalidate-cert',
2361 'notls-secure' => '/notls/secure',
2362 'secure' => '/secure', // for POP3 servers that force CRAM-MD5
2363 'notls' => '/notls',
2364 'none' => '', // try default nothing
2367 'ssl-both-on-secure' => '/ssl/tls/validate-cert/secure',
2368 'ssl-both-on' => '/ssl/tls/validate-cert',
2369 'ssl-cert-secure' => '/ssl/validate-cert/secure',
2370 'ssl-cert' => '/ssl/validate-cert',
2371 'ssl-tls-secure' => '/ssl/tls/secure',
2372 'ssl-tls' => '/ssl/tls',
2373 'ssl-both-off-secure' => '/ssl/notls/novalidate-cert/secure',
2374 'ssl-both-off' => '/ssl/notls/novalidate-cert',
2375 'ssl-nocert-secure' => '/ssl/novalidate-cert/secure',
2376 'ssl-nocert' => '/ssl/novalidate-cert',
2377 'ssl-notls-secure' => '/ssl/notls/secure',
2378 'ssl-notls' => '/ssl/notls',
2379 'ssl-secure' => '/ssl/secure',
2380 'ssl-none' => '/ssl',
2383 if(isset($user) && !empty($user) && isset($pass) && !empty($pass)) {
2384 $this->email_password = $pass;
2385 $this->email_user = $user;
2386 $this->server_url = $server;
2387 $this->port = $port;
2388 $this->protocol = $prot;
2389 $this->mailbox = $mailbox;
2392 // in case we flip from IMAP to POP3
2393 if($this->protocol == 'pop3')
2394 $this->mailbox = 'INBOX';
2396 //If user has selected multiple mailboxes, we only need to test the first mailbox for the connection string.
2397 $a_mailbox = explode(",", $this->mailbox);
2398 $tmpMailbox = isset($a_mailbox[0]) ? $a_mailbox[0] : "";
2402 foreach($ssl as $k => $service)
2404 $returnService[$k] = 'foo'.$service;
2405 $serviceArr[$k] = '{'.$this->server_url.':'.$this->port.'/service='.$this->protocol.$service.'}'.$tmpMailbox;
2410 foreach($nonSsl as $k => $service)
2412 $returnService[$k] = 'foo'.$service;
2413 $serviceArr[$k] = '{'.$this->server_url.':'.$this->port.'/service='.$this->protocol.$service.'}'.$tmpMailbox;
2417 $GLOBALS['log']->debug('---------------STARTING FINDOPTIMUMS LOOP----------------');
2420 //php imap library will capture c-client library warnings as errors causing good connections to be ignored.
2421 //Check against known warnings to ensure good connections are used.
2422 $acceptableWarnings = array("SECURITY PROBLEM: insecure server advertised AUTH=PLAIN", //c-client auth_pla.c
2423 "Mailbox is empty");
2424 $login = $this->email_user;
2425 $passw = $this->email_password;
2426 $foundGoodConnection = false;
2427 foreach($serviceArr as $k => $serviceTest) {
2430 $GLOBALS['log']->debug($l.': I-E testing string: '.$serviceTest);
2432 // open the connection and try the test string
2433 $this->conn = $this->getImapConnection($serviceTest, $login, $passw);
2435 if(($errors = imap_last_error()) || ($alerts = imap_alerts())) {
2436 // login failure means don't bother trying the rest
2437 if ($errors == 'Too many login failures'
2438 || $errors == '[CLOSED] IMAP connection broken (server response)'
2439 // @link http://tools.ietf.org/html/rfc5530#section-3
2440 || strpos($errors, '[AUTHENTICATIONFAILED]') !== false
2442 || (strpos($errors, 'AUTHENTICATE') !== false && strpos($errors, 'failed') !== false)
2444 $GLOBALS['log']->debug($l.': I-E failed using ['.$serviceTest.']');
2445 $retArray['err'][$k] = $mod_strings['ERR_BAD_LOGIN_PASSWORD'];
2446 $retArray['bad'][$k] = $serviceTest;
2447 $GLOBALS['log']->debug($l.': I-E ERROR: $ie->findOptimums() failed due to bad user credentials for user login: '.$this->email_user);
2449 } elseif( in_array($errors, $acceptableWarnings, TRUE)) { // false positive
2450 $GLOBALS['log']->debug($l.': I-E found good connection but with warnings ['.$serviceTest.'] Errors:' . $errors);
2451 $retArray['good'][$k] = $returnService[$k];
2452 $foundGoodConnection = true;
2455 $GLOBALS['log']->debug($l.': I-E failed using ['.$serviceTest.'] - error: '.$errors);
2456 $retArray['err'][$k] = $errors;
2457 $retArray['bad'][$k] = $serviceTest;
2460 $GLOBALS['log']->debug($l.': I-E found good connect using ['.$serviceTest.']');
2461 $retArray['good'][$k] = $returnService[$k];
2462 $foundGoodConnection = true;
2465 if(is_resource($this->conn)) {
2466 if (!$this->isPop3Protocol()) {
2467 $serviceTest = str_replace("INBOX", "", $serviceTest);
2468 $boxes = imap_getmailboxes($this->conn, $serviceTest, "*");
2470 // clean MBOX path names
2471 foreach($boxes as $k => $mbox) {
2472 $raw[] = $mbox->name;
2473 if ($mbox->delimiter) {
2474 $delimiter = $mbox->delimiter;
2477 $this->setSessionInboundDelimiterString($this->server_url, $this->email_user, $this->port, $this->protocol, $delimiter);
2480 if(!imap_close($this->conn)) $GLOBALS['log']->debug('imap_close() failed!');
2483 $GLOBALS['log']->debug($l.': I-E clearing error and alert stacks.');
2484 imap_errors(); // clear stacks
2486 // If you find a good connection, then don't do any further testing to find URL
2487 if ($foundGoodConnection) {
2492 $GLOBALS['log']->debug('---------------end FINDOPTIMUMS LOOP----------------');
2494 if(!empty($retArray['good'])) {
2499 $newNovalidate_cert = '';
2500 $good = array_pop($retArray['good']); // get most complete string
2501 $exGood = explode('/', $good);
2502 foreach($exGood as $v) {
2511 $newNotls = 'notls';
2514 $newCert = 'validate-cert';
2516 case 'novalidate-cert':
2517 $newNovalidate_cert = 'novalidate-cert';
2525 $goodStr['serial'] = $newTls.'::'.$newCert.'::'.$newSsl.'::'.$this->protocol.'::'.$newNovalidate_cert.'::'.$newNotls.'::'.$secure;
2526 $goodStr['service'] = $good;
2527 $testConnectString = str_replace('foo','', $good);
2528 $testConnectString = '{'.$this->server_url.':'.$this->port.'/service='.$this->protocol.$testConnectString.'}';
2529 $this->setSessionConnectionString($this->server_url, $this->email_user, $this->port, $this->protocol, $goodStr);
2531 foreach($raw as $mbox)
2533 $raw[$i] = str_replace($testConnectString, "", $GLOBALS['locale']->translateCharset($mbox, "UTF7-IMAP", "UTF8" ));
2537 $this->setSessionInboundFoldersString($this->server_url, $this->email_user, $this->port, $this->protocol, implode(",", $raw));
2544 function getSessionConnectionString($server_url, $email_user, $port, $protocol) {
2545 $sessionConnectionString = $server_url . $email_user . $port . $protocol;
2546 return (isset($_SESSION[$sessionConnectionString]) ? $_SESSION[$sessionConnectionString] : "");
2549 function setSessionConnectionString($server_url, $email_user, $port, $protocol, $goodStr) {
2550 $sessionConnectionString = $server_url . $email_user . $port . $protocol;
2551 $_SESSION[$sessionConnectionString] = $goodStr;
2554 function getSessionInboundDelimiterString($server_url, $email_user, $port, $protocol) {
2555 $sessionInboundDelimiterString = $server_url . $email_user . $port . $protocol . "delimiter";
2556 return (isset($_SESSION[$sessionInboundDelimiterString]) ? $_SESSION[$sessionInboundDelimiterString] : "");
2559 function setSessionInboundDelimiterString($server_url, $email_user, $port, $protocol, $delimiter) {
2560 $sessionInboundDelimiterString = $server_url . $email_user . $port . $protocol . "delimiter";
2561 $_SESSION[$sessionInboundDelimiterString] = $delimiter;
2564 function getSessionInboundFoldersString($server_url, $email_user, $port, $protocol) {
2565 $sessionInboundFoldersListString = $server_url . $email_user . $port . $protocol . "foldersList";
2566 return (isset($_SESSION[$sessionInboundFoldersListString]) ? $_SESSION[$sessionInboundFoldersListString] : "");
2569 function setSessionInboundFoldersString($server_url, $email_user, $port, $protocol, $foldersList) {
2570 $sessionInboundFoldersListString = $server_url . $email_user . $port . $protocol . "foldersList";
2571 $_SESSION[$sessionInboundFoldersListString] = $foldersList;
2575 * Checks for duplicate Group User names when creating a new one at save()
2576 * @return GUID returns GUID of Group User if user_name match is
2578 * @return boolean false if NO DUPE IS FOUND
2580 function groupUserDupeCheck() {
2581 $q = "SELECT u.id FROM users u WHERE u.deleted=0 AND u.is_group=1 AND u.user_name = '".$this->name."'";
2582 $r = $this->db->query($q, true);
2584 while($a = $this->db->fetchByAssoc($r)) {
2588 if(strlen($uid) > 0) {
2596 * Returns <option> markup with the contents of Group users
2597 * @param array $groups default empty array
2598 * @return string HTML options
2600 function getGroupsWithSelectOptions($groups = array()) {
2601 $r = $this->db->query('SELECT id, user_name FROM users WHERE users.is_group = 1 AND deleted = 0', true);
2602 if(is_resource($r)) {
2603 while($a = $this->db->fetchByAssoc($r)) {
2604 $groups[$a['id']] = $a['user_name'];
2608 $selectOptions = get_select_options_with_id_separate_key($groups, $groups, $this->group_id);
2609 return $selectOptions;
2613 * handles auto-responses to inbound emails
2615 * @param object email Email passed as reference
2617 function handleAutoresponse(&$email, &$contactAddr) {
2618 if($this->template_id) {
2619 $GLOBALS['log']->debug('found auto-reply template id - prefilling and mailing response');
2621 if($this->getAutoreplyStatus($contactAddr)
2622 && $this->checkOutOfOffice($email->name)
2623 && $this->checkFilterDomain($email)) { // if we haven't sent this guy 10 replies in 24hours
2625 if(!empty($this->stored_options)) {
2626 $storedOptions = unserialize(base64_decode($this->stored_options));
2629 if(!empty($storedOptions['from_name'])) {
2630 $from_name = $storedOptions['from_name'];
2631 $GLOBALS['log']->debug('got from_name from storedOptions: '.$from_name);
2632 } else { // use system default
2633 $rName = $this->db->query('SELECT value FROM config WHERE name = \'fromname\'', true);
2634 if(is_resource($rName)) {
2635 $aName = $this->db->fetchByAssoc($rName);
2637 if(!empty($aName['value'])) {
2638 $from_name = $aName['value'];
2644 if(!empty($storedOptions['from_addr'])) {
2645 $from_addr = $storedOptions['from_addr'];
2647 $rAddr = $this->db->query('SELECT value FROM config WHERE name = \'fromaddress\'', true);
2648 if(is_resource($rAddr)) {
2649 $aAddr = $this->db->fetchByAssoc($rAddr);
2651 if(!empty($aAddr['value'])) {
2652 $from_addr = $aAddr['value'];
2658 $replyToName = (!empty($storedOptions['reply_to_name']))? from_html($storedOptions['reply_to_name']) :$from_name ;
2659 $replyToAddr = (!empty($storedOptions['reply_to_addr'])) ? $storedOptions['reply_to_addr'] : $from_addr;
2662 if(!empty($email->reply_to_email)) {
2663 $to[0]['email'] = $email->reply_to_email;
2665 $to[0]['email'] = $email->from_addr;
2667 // handle to name: address, prefer reply-to
2668 if(!empty($email->reply_to_name)) {
2669 $to[0]['display'] = $email->reply_to_name;
2670 } elseif(!empty($email->from_name)) {
2671 $to[0]['display'] = $email->from_name;
2674 $et = new EmailTemplate();
2675 $et->retrieve($this->template_id);
2676 if(empty($et->subject)) { $et->subject = ''; }
2677 if(empty($et->body)) { $et->body = ''; }
2678 if(empty($et->body_html)) { $et->body_html = ''; }
2680 $reply = new Email();
2681 $reply->type = 'out';
2682 $reply->to_addrs = $to[0]['email'];
2683 $reply->to_addrs_arr = $to;
2684 $reply->cc_addrs_arr = array();
2685 $reply->bcc_addrs_arr = array();
2686 $reply->from_name = $from_name;
2687 $reply->from_addr = $from_addr;
2688 $reply->name = $et->subject;
2689 $reply->description = $et->body;
2690 $reply->description_html = $et->body_html;
2691 $reply->reply_to_name = $replyToName;
2692 $reply->reply_to_addr = $replyToAddr;
2694 $GLOBALS['log']->debug('saving and sending auto-reply email');
2695 //$reply->save(); // don't save the actual email.
2697 $this->setAutoreplyStatus($contactAddr);
2699 $GLOBALS['log']->debug('InboundEmail: auto-reply threshold reached for email ('.$contactAddr.') - not sending auto-reply');
2704 function handleCaseAssignment($email) {
2706 if($caseId = $this->getCaseIdFromCaseNumber($email->name, $c)) {
2707 $c->retrieve($caseId);
2708 $email->retrieve($email->id);
2709 //assign the case info to parent id and parent type so that the case can be linked to the email on Email Save
2710 $email->parent_type = "Cases";
2711 $email->parent_id = $caseId;
2712 // assign the email to the case owner
2713 $email->assigned_user_id = $c->assigned_user_id;
2715 $GLOBALS['log']->debug('InboundEmail found exactly 1 match for a case: '.$c->name);
2722 * handles functionality specific to the Mailbox type (Cases, bounced
2725 * @param object email Email object passed as a reference
2726 * @param object header Header object generated by imap_headerinfo();
2728 function handleMailboxType(&$email, &$header) {
2729 switch($this->mailbox_type) {
2731 $this->handleCaseAssignment($email);
2738 // do something with this?
2741 // do something with leads? we don't have an email_leads table
2747 require_once('modules/Campaigns/ProcessBouncedEmails.php');
2748 campaign_process_bounced_emails($email, $header);
2750 case 'pick': // do all except bounce handling
2751 $GLOBALS['log']->debug('looking for a case for '.$email->name);
2752 $this->handleCaseAssignment($email);
2757 function isMailBoxTypeCreateCase() {
2758 return ($this->mailbox_type == 'createcase' && !empty($this->groupfolder_id));
2761 function handleCreateCase($email, $userId) {
2762 global $current_user, $mod_strings, $current_language;
2763 $mod_strings = return_module_language($current_language, "Emails");
2764 $GLOBALS['log']->debug('In handleCreateCase');
2766 $this->getCaseIdFromCaseNumber($email->name, $c);
2768 if (!$this->handleCaseAssignment($email) && $this->isMailBoxTypeCreateCase()) {
2770 $GLOBALS['log']->debug('retrieveing email');
2771 $email->retrieve($email->id);
2773 $c->description = $email->description;
2774 $c->assigned_user_id = $userId;
2775 $c->name = $email->name;
2777 $c->priority = 'P1';
2779 if(!empty($email->reply_to_email)) {
2780 $contactAddr = $email->reply_to_email;
2782 $contactAddr = $email->from_addr;
2785 $GLOBALS['log']->debug('finding related accounts with address ' . $contactAddr);
2786 if($accountIds = $this->getRelatedId($contactAddr, 'accounts')) {
2787 if (sizeof($accountIds) == 1) {
2788 $c->account_id = $accountIds[0];
2790 $acct = new Account();
2791 $acct->retrieve($c->account_id);
2792 $c->account_name = $acct->name;
2798 $c->retrieve($caseId);
2799 if($c->load_relationship('emails')) {
2800 $c->emails->add($email->id);
2802 if($contactIds = $this->getRelatedId($contactAddr, 'contacts')) {
2803 if(!empty($contactIds) && $c->load_relationship('contacts')) {
2804 if (!$accountIds && count($contactIds) == 1) {
2805 $contact = BeanFactory::getBean('Contacts', $contactIds[0]);
2806 if ($contact->load_relationship('accounts')) {
2807 $acct = $contact->accounts->get();
2808 if ($c->load_relationship('accounts') && !empty($acct[0])) {
2809 $c->accounts->add($acct[0]);
2813 $c->contacts->add($contactIds);
2816 $c->email_id = $email->id;
2817 $email->parent_type = "Cases";
2818 $email->parent_id = $caseId;
2819 // assign the email to the case owner
2820 $email->assigned_user_id = $c->assigned_user_id;
2821 $email->name = str_replace('%1', $c->case_number, $c->getEmailSubjectMacro()) . " ". $email->name;
2823 $GLOBALS['log']->debug('InboundEmail created one case with number: '.$c->case_number);
2824 $createCaseTemplateId = $this->get_stored_options('create_case_email_template', "");
2825 if(!empty($this->stored_options)) {
2826 $storedOptions = unserialize(base64_decode($this->stored_options));
2828 if(!empty($createCaseTemplateId)) {
2831 if (!empty($this->stored_options)) {
2832 $fromAddress = $storedOptions['from_addr'];
2833 $fromName = from_html($storedOptions['from_name']);
2834 $replyToName = (!empty($storedOptions['reply_to_name']))? from_html($storedOptions['reply_to_name']) :$fromName ;
2835 $replyToAddr = (!empty($storedOptions['reply_to_addr'])) ? $storedOptions['reply_to_addr'] : $fromAddress;
2837 $defaults = $current_user->getPreferredEmail();
2838 $fromAddress = (!empty($fromAddress)) ? $fromAddress : $defaults['email'];
2839 $fromName = (!empty($fromName)) ? $fromName : $defaults['name'];
2840 $to[0]['email'] = $contactAddr;
2842 // handle to name: address, prefer reply-to
2843 if(!empty($email->reply_to_name)) {
2844 $to[0]['display'] = $email->reply_to_name;
2845 } elseif(!empty($email->from_name)) {
2846 $to[0]['display'] = $email->from_name;
2849 $et = new EmailTemplate();
2850 $et->retrieve($createCaseTemplateId);
2851 if(empty($et->subject)) { $et->subject = ''; }
2852 if(empty($et->body)) { $et->body = ''; }
2853 if(empty($et->body_html)) { $et->body_html = ''; }
2855 $et->subject = "Re:" . " " . str_replace('%1', $c->case_number, $c->getEmailSubjectMacro() . " ". $c->name);
2857 $html = trim($email->description_html);
2858 $plain = trim($email->description);
2860 $email->email2init();
2861 $email->from_addr = $email->from_addr_name;
2862 $email->to_addrs = $email->to_addrs_names;
2863 $email->cc_addrs = $email->cc_addrs_names;
2864 $email->bcc_addrs = $email->bcc_addrs_names;
2865 $email->from_name = $email->from_addr;
2867 $email = $email->et->handleReplyType($email, "reply");
2868 $ret = $email->et->displayComposeEmail($email);
2869 $ret['description'] = empty($email->description_html) ? str_replace("\n", "\n<BR/>", $email->description) : $email->description_html;
2871 $reply = new Email();
2872 $reply->type = 'out';
2873 $reply->to_addrs = $to[0]['email'];
2874 $reply->to_addrs_arr = $to;
2875 $reply->cc_addrs_arr = array();
2876 $reply->bcc_addrs_arr = array();
2877 $reply->from_name = $fromName;
2878 $reply->from_addr = $fromAddress;
2879 $reply->reply_to_name = $replyToName;
2880 $reply->reply_to_addr = $replyToAddr;
2881 $reply->name = $et->subject;
2882 $reply->description = $et->body . "<div><hr /></div>" . $email->description;
2883 if (!$et->text_only) {
2884 $reply->description_html = $et->body_html . "<div><hr /></div>" . $email->description;
2886 $GLOBALS['log']->debug('saving and sending auto-reply email');
2887 //$reply->save(); // don't save the actual email.
2892 if(!empty($email->reply_to_email)) {
2893 $contactAddr = $email->reply_to_email;
2895 $contactAddr = $email->from_addr;
2897 $this->handleAutoresponse($email, $contactAddr);
2903 * handles linking contacts, accounts, etc. to an email
2905 * @param object Email bean to be linked against
2906 * @return string contactAddr is the email address of the sender
2908 function handleLinking(&$email) {
2909 // link email to an User if emails match TO addr
2910 if($userIds = $this->getRelatedId($email->to_addrs, 'users')) {
2911 $GLOBALS['log']->debug('I-E linking email to User');
2912 // link the user to the email
2913 $email->load_relationship('users');
2914 $email->users->add($userIds);
2917 // link email to a Contact, Lead, or Account if the emails match
2918 // give precedence to REPLY-TO above FROM
2919 if(!empty($email->reply_to_email)) {
2920 $contactAddr = $email->reply_to_email;
2922 $contactAddr = $email->from_addr;
2925 // Samir Gandhi : 12/06/07
2926 // This changes has been done because the linking was done only with the from address and
2927 // not with to address
2928 $relationShipAddress = $contactAddr;
2929 if (empty($relationShipAddress)) {
2930 $relationShipAddress .= $email->to_addrs;
2932 $relationShipAddress = $relationShipAddress . "," . $email->to_addrs;
2934 if($leadIds = $this->getRelatedId($relationShipAddress, 'leads')) {
2935 $GLOBALS['log']->debug('I-E linking email to Lead');
2936 $email->load_relationship('leads');
2937 $email->leads->add($leadIds);
2939 foreach($leadIds as $leadId) {
2941 $lead->retrieve($leadId);
2942 $lead->load_relationship('emails');
2943 $lead->emails->add($email->id);
2947 if($contactIds = $this->getRelatedId($relationShipAddress, 'contacts')) {
2948 $GLOBALS['log']->debug('I-E linking email to Contact');
2949 // link the contact to the email
2950 $email->load_relationship('contacts');
2951 $email->contacts->add($contactIds);
2954 if($accountIds = $this->getRelatedId($relationShipAddress, 'accounts')) {
2955 $GLOBALS['log']->debug('I-E linking email to Account');
2956 // link the account to the email
2957 $email->load_relationship('accounts');
2958 $email->accounts->add($accountIds);
2960 /* cn: bug 9171 another cause of dying I-E - bad linking
2961 foreach($accountIds as $accountId) {
2962 $GLOBALS['log']->debug('I-E reverse-linking Accounts to Emails');
2963 $acct = new Account();
2964 $acct->retrieve($accountId);
2965 $acct->load_relationship('emails');
2966 $acct->account_emails->add($email->id);
2970 return $contactAddr;
2974 * Gets part by following breadcrumb path
2975 * @param string $bc the breadcrumb string in format (1.1.1)
2976 * @param array parts the root level parts array
2978 protected function getPartByPath($bc, $parts)
2980 if(strstr($bc,'.')) {
2981 $exBc = explode('.', $bc);
2986 foreach($exBc as $step) {
2987 if(empty($parts)) return false;
2988 $res = $parts[$step-1]; // MIME starts with 1, array starts with 0
2989 if(!empty($res->parts)) {
2990 $parts = $res->parts;
2999 * takes a breadcrumb and returns the encoding at that level
3000 * @param string bc the breadcrumb string in format (1.1.1)
3001 * @param array parts the root level parts array
3002 * @return int retInt Int key to transfer encoding (see handleTranserEncoding())
3004 function getEncodingFromBreadCrumb($bc, $parts) {
3005 if(strstr($bc,'.')) {
3006 $exBc = explode('.', $bc);
3011 $depth = count($exBc);
3013 for($i=0; $i<$depth; $i++) {
3014 $tempObj[$i] = $parts[($exBc[$i]-1)];
3015 $retInt = imap_utf8($tempObj[$i]->encoding);
3016 if(!empty($tempObj[$i]->parts)) {
3017 $parts = $tempObj[$i]->parts;
3024 * retrieves the charset for a given part of an email body
3026 * @param string bc target part of the message in format (1.1.1)
3027 * @param array parts 1 level above ROOT array of Objects representing a multipart body
3028 * @return string charset name
3030 function getCharsetFromBreadCrumb($bc, $parts)
3032 $tempObj = $this->getPartByPath($bc, $parts);
3033 // now we have the tempObj at the end of the breadCrumb trail
3035 if(!empty($tempObj->ifparameters)) {
3036 foreach($tempObj->parameters as $param) {
3037 if(strtolower($param->attribute) == 'charset') {
3038 return $param->value;
3047 * Get the message text from a single mime section, html or plain.
3049 * @param string $msgNo
3050 * @param string $section
3051 * @param stdObject $structure
3054 function getMessageTextFromSingleMimePart($msgNo,$section,$structure)
3056 $msgPartTmp = imap_fetchbody($this->conn, $msgNo, $section);
3057 $enc = $this->getEncodingFromBreadCrumb($section, $structure->parts);
3058 $charset = $this->getCharsetFromBreadCrumb($section, $structure->parts);
3059 $msgPartTmp = $this->handleTranserEncoding($msgPartTmp, $enc);
3060 return $this->handleCharsetTranslation($msgPartTmp, $charset);
3064 * Givin an existing breadcrumb add a cooresponding offset
3067 * @param string $offset
3070 function addBreadCrumbOffset($bc, $offset)
3072 if( (empty($bc) || is_null($bc)) && !empty($offset) )
3075 $a_bc = explode(".", $bc);
3076 $a_offset = explode(".",$offset);
3077 if(count($a_bc) < count($a_offset))
3078 $a_bc = array_merge($a_bc,array_fill( count($a_bc), count($a_offset) - count($a_bc), 0));
3081 for($i=0;$i < count($a_bc); $i++)
3083 if(isset($a_offset[$i]))
3084 $results[] = $a_bc[$i] + $a_offset[$i];
3086 $results[] = $a_bc[$i];
3088 return implode(".", $results);
3092 * returns the HTML text part of a multi-part message
3094 * @param int msgNo the relative message number for the monitored mailbox
3095 * @param string $type the type of text processed, either 'PLAIN' or 'HTML'
3096 * @return string UTF-8 encoded version of the requested message text
3098 function getMessageText($msgNo, $type, $structure, $fullHeader,$clean_email=true, $bcOffset = "") {
3099 global $sugar_config;
3102 $bc = $this->buildBreadCrumbs($structure->parts, $type);
3103 //Add an offset if specified
3104 if(!empty($bcOffset))
3105 $bc = $this->addBreadCrumbOffset($bc, $bcOffset);
3107 if(!empty($bc)) { // multi-part
3108 // HUGE difference between PLAIN and HTML
3109 if($type == 'PLAIN') {
3110 $msgPart = $this->getMessageTextFromSingleMimePart($msgNo,$bc,$structure);
3112 // get part of structure that will
3114 $bcArray = $this->buildBreadCrumbsHTML($structure->parts,$bcOffset);
3115 // construct inline HTML/Rich msg
3116 foreach($bcArray as $bcArryKey => $bcArr) {
3117 foreach($bcArr as $type => $bcTrail) {
3119 $msgPartRaw .= $this->getMessageTextFromSingleMimePart($msgNo,$bcTrail,$structure);
3121 // deal with inline image
3122 $part = $this->getPartByPath($bcTrail, $structure->parts);
3123 if(empty($part) || empty($part->id)) continue;
3124 $partid = substr($part->id, 1, -1); // strip <> around
3125 if(isset($this->inlineImages[$partid])) {
3126 $imageName = $this->inlineImages[$partid];
3127 $newImagePath = "class=\"image\" src=\"{$this->imagePrefix}{$imageName}\"";
3128 $preImagePath = "src=\"cid:$partid\"";
3129 $msgPartRaw = str_replace($preImagePath, $newImagePath, $msgPartRaw);
3134 $msgPart = $msgPartRaw;
3136 } else { // either PLAIN message type (flowed) or b0rk3d RFC
3137 // make sure we're working on valid data here.
3138 if($structure->subtype != $type) {
3142 $decodedHeader = $this->decodeHeader($fullHeader);
3144 // now get actual body contents
3145 $text = imap_body($this->conn, $msgNo);
3147 $upperCaseKeyDecodeHeader = array();
3148 if (is_array($decodedHeader)) {
3149 $upperCaseKeyDecodeHeader = array_change_key_case($decodedHeader, CASE_UPPER);
3151 if(isset($upperCaseKeyDecodeHeader[strtoupper('Content-Transfer-Encoding')])) {
3152 $flip = array_flip($this->transferEncoding);
3153 $text = $this->handleTranserEncoding($text, $flip[strtoupper($upperCaseKeyDecodeHeader[strtoupper('Content-Transfer-Encoding')])]);
3156 if(is_array($upperCaseKeyDecodeHeader['CONTENT-TYPE']) && isset($upperCaseKeyDecodeHeader['CONTENT-TYPE']['charset']) && !empty($upperCaseKeyDecodeHeader['CONTENT-TYPE']['charset'])) {
3157 // we have an explicit content type, use it
3158 $msgPart = $this->handleCharsetTranslation($text, $upperCaseKeyDecodeHeader['CONTENT-TYPE']['charset']);
3160 // make a best guess as to what our content type is
3161 $msgPart = $this->convertToUtf8($text);
3163 } // end else clause
3165 $msgPart = $this->customGetMessageText($msgPart);
3166 /* cn: bug 9176 - htmlEntitites hide XSS attacks. */
3167 if($type == 'PLAIN') {
3168 return SugarCleaner::cleanHtml(to_html($msgPart), false);
3170 // Bug 50241: can't process <?xml:namespace .../> properly. Strip <?xml ...> tag first.
3171 $msgPart = preg_replace("/<\?xml[^>]*>/","",$msgPart);
3173 return SugarCleaner::cleanHtml($msgPart, false);
3177 * decodes raw header information and passes back an associative array with
3178 * the important elements key'd by name
3179 * @param header string the raw header
3180 * @return decodedHeader array the associative array
3182 function decodeHeader($fullHeader) {
3183 $decodedHeader = array();
3184 $exHeaders = explode("\r", $fullHeader);
3185 if (!is_array($exHeaders)) {
3186 $exHeaders = explode("\r\n", $fullHeader);
3188 $quotes = array('"', "'");
3190 foreach($exHeaders as $lineNum => $head) {
3192 $key = trim(substr($head, 0, strpos($head, ':')));
3194 $value = trim(substr($head, (strpos($head, ':') + 1), strlen($head)));
3196 // handle content-type section in headers
3197 if(strtolower($key) == 'content-type' && strpos($value, ';')) { // ";" means something follows related to (such as Charset)
3198 $semiColPos = mb_strpos($value, ';');
3199 $strLenVal = mb_strlen($value);
3200 if(($semiColPos + 4) >= $strLenVal) {
3201 // the charset="[something]" is on the next line
3202 $value .= str_replace($quotes, "", trim($exHeaders[$lineNum+1]));
3205 $newValue = array();
3206 $exValue = explode(';', $value);
3207 $newValue['type'] = $exValue[0];
3209 for($i=1; $i<count($exValue); $i++) {
3210 $exContent = explode('=', $exValue[$i]);
3211 $newValue[trim($exContent[0])] = trim($exContent[1], "\t \"");
3216 if(!empty($key) && !empty($value)) {
3217 $decodedHeader[$key] = $value;
3221 return $decodedHeader;
3225 * handles translating message text from orignal encoding into UTF-8
3227 * @param string text test to be re-encoded
3228 * @param string charset original character set
3229 * @return string utf8 re-encoded text
3231 function handleCharsetTranslation($text, $charset) {
3234 if(empty($charset)) {
3235 $GLOBALS['log']->debug("***ERROR: InboundEmail::handleCharsetTranslation() called without a \$charset!");
3236 $GLOBALS['log']->debug("***STACKTRACE: ".print_r(debug_backtrace(), true));
3240 // typical headers have no charset - let destination pick (since it's all ASCII anyways)
3241 if(strtolower($charset) == 'default' || strtolower($charset) == 'utf-8') {
3245 return $locale->translateCharset($text, $charset);
3251 * Builds up the "breadcrumb" trail that imap_fetchbody() uses to return
3252 * parts of an email message, including attachments and inline images
3253 * @param $parts array of objects
3254 * @param $subtype what type of trail to return? HTML? Plain? binaries?
3255 * @param $breadcrumb text trail to build up
3257 function buildBreadCrumbs($parts, $subtype, $breadcrumb = '0') {
3258 //_pp('buildBreadCrumbs building for '.$subtype.' with BC at '.$breadcrumb);
3259 // loop through available parts in the array
3260 foreach($parts as $k => $part) {
3261 // mark passage through level
3263 // if this is not the first time through, start building the map
3264 if($breadcrumb != 0) {
3265 $thisBc = $breadcrumb.'.'.$thisBc;
3268 // found a multi-part/mixed 'part' - keep digging
3269 if($part->type == 1 && (strtoupper($part->subtype) == 'RELATED' || strtoupper($part->subtype) == 'ALTERNATIVE' || strtoupper($part->subtype) == 'MIXED')) {
3270 //_pp('in loop: going deeper with subtype: '.$part->subtype.' $k is: '.$k);
3271 $thisBc = $this->buildBreadCrumbs($part->parts, $subtype, $thisBc);
3274 } elseif(strtolower($part->subtype) == strtolower($subtype)) { // found the subtype we want, return the breadcrumb value
3275 //_pp('found '.$subtype.' bc! returning: '.$thisBc);
3278 //_pp('found '.$part->subtype.' instead');
3284 * Similar to buildBreadCrumbs() but returns an ordered array containing all parts of the message that would be
3285 * considered "HTML" or Richtext (embedded images, formatting, etc.).
3286 * @param array parts Array of parts of a message
3287 * @param int breadcrumb Passed integer value to start breadcrumb trail
3288 * @param array stackedBreadcrumbs Persistent trail of breadcrumbs
3289 * @return array Ordered array of parts to retrieve via imap_fetchbody()
3291 function buildBreadCrumbsHTML($parts, $breadcrumb = '0', $stackedBreadcrumbs = array()) {
3293 $disposition = 'inline';
3295 foreach($parts as $k => $part) {
3296 // mark passage through level
3299 if($breadcrumb != 0) {
3300 $thisBc = $breadcrumb.'.'.$thisBc;
3302 // found a multi-part/mixed 'part' - keep digging
3303 if($part->type == 1 && (strtoupper($part->subtype) == 'RELATED' || strtoupper($part->subtype) == 'ALTERNATIVE' || strtoupper($part->subtype) == 'MIXED')) {
3304 $stackedBreadcrumbs = $this->buildBreadCrumbsHTML($part->parts, $thisBc, $stackedBreadcrumbs);
3306 (strtolower($part->subtype) == strtolower($subtype)) ||
3308 isset($part->disposition) && strtolower($part->disposition) == 'inline' &&
3309 in_array(strtoupper($part->subtype), $this->imageTypes)
3312 // found the subtype we want, return the breadcrumb value
3313 $stackedBreadcrumbs[] = array(strtolower($part->subtype) => $thisBc);
3314 } elseif($part->type == 5) {
3315 $stackedBreadcrumbs[] = array(strtolower($part->subtype) => $thisBc);
3319 return $stackedBreadcrumbs;
3323 * Takes a PHP imap_* object's to/from/cc/bcc address field and converts it
3324 * to a standard string that SugarCRM expects
3325 * @param $arr an array of email address objects
3327 function convertImapToSugarEmailAddress($arr) {
3328 if(is_array($arr)) {
3330 foreach($arr as $key => $obj) {
3331 $addr .= $obj->mailbox.'@'.$obj->host.', ';
3334 $ret = substr_replace($addr,'',-2,-1);
3340 * tries to figure out what character set a given filename is using and
3341 * decode based on that
3343 * @param string name Name of attachment
3344 * @return string decoded name
3346 function handleEncodedFilename($name) {
3347 $imapDecode = imap_mime_header_decode($name);
3348 /******************************
3349 $imapDecode => stdClass Object
3352 [text] => w�hlen.php
3357 $imapDecode => stdClass Object
3359 [charset] => default
3360 [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
3362 *******************************/
3363 if($imapDecode[0]->charset != 'default') { // mime-header encoded charset
3364 $encoding = $imapDecode[0]->charset;
3365 $name = $imapDecode[0]->text; // encoded in that charset
3367 /* encoded filenames are formatted as [encoding]''[filename] */
3368 if(strpos($name, "''") !== false) {
3370 $encoding = substr($name, 0, strpos($name, "'"));
3372 while(strpos($name, "'") !== false) {
3373 $name = trim(substr($name, (strpos($name, "'")+1), strlen($name)));
3376 $name = urldecode($name);
3378 return (strtolower($encoding) == 'utf-8') ? $name : $GLOBALS['locale']->translateCharset($name, $encoding, 'UTF-8');
3382 Primary body types for a part of a mail structure (imap_fetchstructure returned object)
3394 Primary body types for a part of a mail structure (imap_fetchstructure returned object)
3395 @var array $imap_types
3397 public $imap_types = array(
3407 public function getMimeType($type, $subtype)
3409 if(isset($this->imap_types[$type])) {
3410 return $this->imap_types[$type]."/$subtype";
3412 return "other/$subtype";
3417 * Takes the "parts" attribute of the object that imap_fetchbody() method
3418 * returns, and recursively goes through looking for objects that have a
3419 * disposition of "attachement" or "inline"
3420 * @param int $msgNo The relative message number for the monitored mailbox
3421 * @param object $parts Array of objects to examine
3422 * @param string $emailId The GUID of the email saved prior to calling this method
3423 * @param array $breadcrumb Default 0, build up of the parts mapping
3424 * @param bool $forDisplay Default false
3426 function saveAttachments($msgNo, $parts, $emailId, $breadcrumb='0', $forDisplay) {
3427 global $sugar_config;
3429 Primary body types for a part of a mail structure (imap_fetchstructure returned object)
3440 foreach($parts as $k => $part) {
3442 if($breadcrumb != '0') {
3443 $thisBc = $breadcrumb.'.'.$thisBc;
3446 // check if we need to recurse into the object
3447 //if($part->type == 1 && !empty($part->parts)) {
3448 if(isset($part->parts) && !empty($part->parts) && !( isset($part->subtype) && strtolower($part->subtype) == 'rfc822') ) {
3449 $this->saveAttachments($msgNo, $part->parts, $emailId, $thisBc, $forDisplay);
3451 } elseif($part->ifdisposition) {
3452 // we will take either 'attachments' or 'inline'
3453 if(strtolower($part->disposition) == 'attachment' || ((strtolower($part->disposition) == 'inline') && $part->type != 0)) {
3454 $attach = $this->getNoteBeanForAttachment($emailId);
3455 $fname = $this->handleEncodedFilename($this->retrieveAttachmentNameFromStructure($part));
3457 if(!empty($fname)) {//assign name to attachment
3458 $attach->name = $fname;
3459 } else {//if name is empty, default to filename
3460 $attach->name = urlencode($this->retrieveAttachmentNameFromStructure($part));
3462 $attach->filename = $attach->name;
3463 if (empty($attach->filename)) {
3467 // deal with the MIME types email has
3468 $attach->file_mime_type = $this->getMimeType($part->type, $part->subtype);
3469 $attach->safeAttachmentName();
3471 $attach->id = $this->getTempFilename();
3473 // only save if doing a full import, else we want only the binaries
3476 } // end if disposition type 'attachment'
3477 }// end ifdisposition
3478 //Retrieve contents of subtype rfc8822
3479 elseif ($part->type == 2 && isset($part->subtype) && strtolower($part->subtype) == 'rfc822' )
3481 $tmp_eml = imap_fetchbody($this->conn, $msgNo, $thisBc);
3482 $attach = $this->getNoteBeanForAttachment($emailId);
3483 $attach->file_mime_type = 'messsage/rfc822';
3484 $attach->description = $tmp_eml;
3485 $attach->filename = 'bounce.eml';
3486 $attach->safeAttachmentName();
3488 $attach->id = $this->getTempFilename();
3490 // only save if doing a full import, else we want only the binaries
3493 } elseif(!$part->ifdisposition && $part->type != 1 && $part->type != 2 && $thisBc != '1') {
3494 // No disposition here, but some IMAP servers lie about disposition headers, try to find the truth
3495 // Also Outlook puts inline attachments as type 5 (image) without a disposition
3496 if($part->ifparameters) {
3497 foreach($part->parameters as $param) {
3498 if(strtolower($param->attribute) == "name" || strtolower($param->attribute) == "filename") {
3499 $fname = $this->handleEncodedFilename($param->value);
3503 if(empty($fname)) continue;
3505 // we assume that named parts are attachments too
3506 $attach = $this->getNoteBeanForAttachment($emailId);
3508 $attach->filename = $attach->name = $fname;
3509 $attach->file_mime_type = $this->getMimeType($part->type, $part->subtype);
3511 $attach->safeAttachmentName();
3513 $attach->id = $this->getTempFilename();
3515 // only save if doing a full import, else we want only the binaries
3520 $this->saveAttachmentBinaries($attach, $msgNo, $thisBc, $part, $forDisplay);
3525 * Return a new note object for attachments.
3527 * @param string $emailId
3530 function getNoteBeanForAttachment($emailId)
3532 $attach = new Note();
3533 $attach->parent_id = $emailId;
3534 $attach->parent_type = 'Emails';
3540 * Return the filename of the attachment by examining the dparameters or parameters returned from imap_fetch_structure
3542 * @param object $part
3545 function retrieveAttachmentNameFromStructure($part)
3549 foreach ($part->dparameters as $k => $v)
3551 if( strtolower($v->attribute) == 'filename')
3553 $result = $v->value;
3558 if (empty($result)) {
3559 foreach ($part->parameters as $k => $v) {
3560 if (strtolower($v->attribute) == 'name') {
3561 $result = $v->value;
3571 * saves the actual binary file of a given attachment
3572 * @param object attach Note object that is attached to the binary file
3573 * @param string msgNo Message Number on IMAP/POP3 server
3574 * @param string thisBc Breadcrumb to navigate email structure to find the content
3575 * @param object part IMAP standard object that contains the "parts" of this section of email
3576 * @param bool $forDisplay
3578 function saveAttachmentBinaries($attach, $msgNo, $thisBc, $part, $forDisplay) {
3579 // decide where to place the file temporarily
3580 $uploadDir = ($forDisplay) ? "{$this->EmailCachePath}/{$this->id}/attachments/" : "upload://";
3582 // decide what name to save file as
3583 $fileName = $attach->id;
3585 // download the attachment if we didn't do it yet
3586 if(!file_exists($uploadDir.$fileName)) {
3587 $msgPartRaw = imap_fetchbody($this->conn, $msgNo, $thisBc);
3588 // deal with attachment encoding and decode the text string
3589 $msgPart = $this->handleTranserEncoding($msgPartRaw, $part->encoding);
3591 if(file_put_contents($uploadDir.$fileName, $msgPart)) {
3592 $GLOBALS['log']->debug('InboundEmail saved attachment file: '.$attach->filename);
3594 $GLOBALS['log']->debug('InboundEmail could not create attachment file: '.$attach->filename ." - temp file target: [ {$uploadDir}{$fileName} ]");
3599 $this->tempAttachment[$fileName] = urldecode($attach->filename);
3600 // if all was successful, feel for inline and cache Note ID for display:
3601 if((strtolower($part->disposition) == 'inline' && in_array($part->subtype, $this->imageTypes))
3602 || ($part->type == 5)) {
3603 if(copy($uploadDir.$fileName, sugar_cached("images/{$fileName}.").strtolower($part->subtype))) {
3604 $id = substr($part->id, 1, -1); //strip <> around
3605 $this->inlineImages[$id] = $attach->id.".".strtolower($part->subtype);
3607 $GLOBALS['log']->debug('InboundEmail could not copy '.$uploadDir.$fileName.' to cache');
3613 * decodes a string based on its associated encoding
3614 * if nothing is passed, we default to no-encoding type
3615 * @param $str encoded string
3616 * @param $enc detected encoding
3618 function handleTranserEncoding($str, $enc=0) {
3624 $ret = base64_decode($str);
3626 case 4:// QUOTED-PRINTABLE
3627 $ret = quoted_printable_decode($str);
3629 case 0:// 7BIT or 8BIT
3630 case 1:// already in a string-useable format - do nothing
3632 default:// catch all
3642 * Some emails do not get assigned a message_id, specifically from
3645 * We need to derive a reliable one for duplicate import checking.
3647 function getMessageId($header) {
3648 $message_id = md5(print_r($header, true));
3653 * checks for duplicate emails on polling. The uniqueness of a given email message is determined by a concatenation
3654 * of 2 values, the messageID and the delivered-to field. This allows multiple To: and B/CC: destination addresses
3655 * to be imported by Sugar without violating the true duplicate-email issues.
3657 * @param string message_id message ID generated by sending server
3658 * @param int message number (mailserver's key) of email
3659 * @param object header object generated by imap_headerinfo()
3660 * @param string textHeader Headers in normal text format
3663 function importDupeCheck($message_id, $header, $textHeader) {
3664 $GLOBALS['log']->debug('*********** InboundEmail doing dupe check.');
3666 // generate "delivered-to" seed for email duplicate check
3667 $deliveredTo = $this->id; // cn: bug 12236 - cc's failing dupe check
3668 $exHeader = explode("\n", $textHeader);
3670 foreach($exHeader as $headerLine) {
3671 if(strpos(strtolower($headerLine), 'delivered-to:') !== false) {
3672 $deliveredTo = substr($headerLine, strpos($headerLine, " "), strlen($headerLine));
3673 $GLOBALS['log']->debug('********* InboundEmail found [ '.$deliveredTo.' ] as the destination address for email [ '.$message_id.' ]');
3674 } elseif(strpos(strtolower($headerLine), 'x-real-to:') !== false) {
3675 $deliveredTo = substr($headerLine, strpos($headerLine, " "), strlen($headerLine));
3676 $GLOBALS['log']->debug('********* InboundEmail found [ '.$deliveredTo.' ] for non-standards compliant email x-header [ '.$message_id.' ]');
3680 //if(empty($message_id) && !isset($message_id)) {
3681 if(empty($message_id) || !isset($message_id)) {
3682 $GLOBALS['log']->debug('*********** NO MESSAGE_ID.');
3683 $message_id = $this->getMessageId($header);
3686 // generate compound messageId
3687 $this->compoundMessageId = trim($message_id).trim($deliveredTo);
3688 if (empty($this->compoundMessageId)) {
3689 $GLOBALS['log']->error('Inbound Email found a message without a header and message_id');
3692 $this->compoundMessageId = md5($this->compoundMessageId);
3694 $query = 'SELECT count(emails.id) AS c FROM emails WHERE emails.message_id = \''.$this->compoundMessageId.'\' and emails.deleted = 0';
3695 $r = $this->db->query($query, true);
3696 $a = $this->db->fetchByAssoc($r);
3699 $GLOBALS['log']->debug('InboundEmail found a duplicate email with ID ('.$this->compoundMessageId.')');
3700 return false; // we have a dupe and don't want to import the email'
3707 * takes the output from imap_mime_hader_decode() and handles multiple types of encoding
3708 * @param string subject Raw subject string from email
3709 * @return string ret properly formatted UTF-8 string
3711 function handleMimeHeaderDecode($subject) {
3712 $subjectDecoded = imap_mime_header_decode($subject);
3715 foreach($subjectDecoded as $object) {
3716 if($object->charset != 'default') {
3717 $ret .= $this->handleCharsetTranslation($object->text, $object->charset);
3719 $ret .= $object->text;
3726 * Calculates the appropriate display date/time sent for an email.
3727 * @param string headerDate The date sent of email in MIME header format
3728 * @return string GMT-0 Unix timestamp
3730 function getUnixHeaderDate($headerDate) {
3733 if (empty($headerDate)) {
3736 ///////////////////////////////////////////////////////////////////
3737 //// CALCULATE CORRECT SENT DATE/TIME FOR EMAIL
3738 if(!empty($headerDate)) {
3739 // Bug 25254 - Strip trailing space that come in some header dates (maybe ones with 1-digit day number)
3740 $headerDate = trim($headerDate);
3741 // need to hack PHP/windows' bad handling of strings when using POP3
3742 if(strstr($headerDate,'+0000 GMT')) {
3743 $headerDate = str_replace('GMT','', $headerDate);
3744 } elseif(!strtotime($headerDate)) {
3745 $headerDate = 'now'; // catch non-standard format times.
3747 // cn: bug 9196 parse the GMT offset
3748 if(strpos($headerDate, '-') || strpos($headerDate, '+')) {
3749 // cn: bug make sure last 5 chars are [+|-]nnnn
3750 if(strpos($headerDate, "(")) {
3751 $headerDate = preg_replace('/\([\w]+\)/i', "", $headerDate);
3752 $headerDate = trim($headerDate);
3755 // parse mailserver time
3756 $gmtEmail = trim(substr($headerDate, -5, 5));
3757 $posNeg = substr($gmtEmail, 0, 1);
3758 $gmtHours = substr($gmtEmail, 1, 2);
3759 $gmtMins = substr($gmtEmail, -2, 2);
3762 $secsHours = $gmtHours * 60 * 60;
3763 $secsTotal = $secsHours + ($gmtMins * 60);
3764 $secsTotal = ($posNeg == '-') ? $secsTotal : -1 * $secsTotal;
3766 $headerDate = trim(substr_replace($headerDate, '', -5)); // mfh: bug 10961/12855 - date time values with GMT offsets not properly formatted
3770 $headerDate = 'now';
3773 $unixHeaderDate = strtotime($headerDate);
3775 if(isset($secsTotal)) {
3776 // this gets the timestamp to true GMT-0
3777 $unixHeaderDate += $secsTotal;
3780 if(strtotime('Jan 1, 2001') > $unixHeaderDate) {
3781 $unixHeaderDate = strtotime('now');
3784 return $unixHeaderDate;
3785 //// END CALCULATE CORRECT SENT DATE/TIME FOR EMAIL
3786 ///////////////////////////////////////////////////////////////////
3790 * This method returns the correct messageno for the pop3 protocol
3791 * @param String UIDL
3792 * @return returnMsgNo
3794 function getCorrectMessageNoForPop3($messageId) {
3796 if ($this->protocol == 'pop3') {
3797 if($this->pop3_open()) {
3798 // get the UIDL from database;
3799 $query = "SELECT msgno FROM email_cache WHERE ie_id = '{$this->id}' AND message_id = '{$messageId}'";
3800 $r = $this->db->query($query);
3801 $a = $this->db->fetchByAssoc($r);
3802 $msgNo = $a['msgno'];
3803 $returnMsgNo = $msgNo;
3806 $this->pop3_sendCommand("USER", $this->email_user);
3807 $this->pop3_sendCommand("PASS", $this->email_password);
3809 // get UIDL for this msgNo
3810 $this->pop3_sendCommand("UIDL {$msgNo}", '', false); // leave socket buffer alone until the while()
3811 $buf = fgets($this->pop3socket, 1024); // handle "OK+ msgNo UIDL(UIDL for this messageno)";
3813 // if it returns OK then we have found the message else get all the UIDL
3814 // and search for the correct msgNo;
3815 $foundMessageNo = false;
3816 if (preg_match("/OK/", $buf) > 0) {
3817 $mailserverResponse = explode(" ", $buf);
3818 // if the cachedUIDL and the UIDL from mail server matches then its the correct messageno
3819 if (trim($mailserverResponse[sizeof($mailserverResponse) - 1]) == $messageId) {
3820 $foundMessageNo = true;
3824 //get all the UIDL and then find the correct messageno
3825 if (!$foundMessageNo) {
3827 $this->pop3_sendCommand("UIDL", '', false); // leave socket buffer alone until the while()
3828 fgets($this->pop3socket, 1024); // handle "OK+";
3831 if(is_resource($this->pop3socket)) {
3832 while(!feof($this->pop3socket)) {
3833 $buf = fgets($this->pop3socket, 1024); // 8kb max buffer - shouldn't be more than 80 chars via pop3...
3834 if(trim($buf) == '.') {
3835 $GLOBALS['log']->debug("*** GOT '.'");
3838 // format is [msgNo] [UIDL]
3839 $exUidl = explode(" ", $buf);
3840 $UIDLs[trim($exUidl[1])] = trim($exUidl[0]);
3842 if (array_key_exists($messageId, $UIDLs)) {
3843 $returnMsgNo = $UIDLs[$messageId];
3845 // message could not be found on server
3851 $this->pop3_cleanUp();
3854 return $returnMsgNo;
3858 * If the importOneEmail returns false, then findout if the duplicate email
3860 function getDuplicateEmailId($msgNo, $uid) {
3862 global $app_strings;
3863 global $app_list_strings;
3864 global $sugar_config;
3865 global $current_user;
3867 $header = imap_headerinfo($this->conn, $msgNo);
3868 $fullHeader = imap_fetchheader($this->conn, $msgNo); // raw headers
3870 // reset inline images cache
3871 $this->inlineImages = array();
3873 // handle messages deleted on server
3874 if(empty($header)) {
3875 if(!isset($this->email) || empty($this->email)) {
3876 $this->email = new Email();
3880 $dupeCheckResult = $this->importDupeCheck($header->message_id, $header, $fullHeader);
3881 if (!$dupeCheckResult && !empty($this->compoundMessageId)) {
3882 // we have a duplicate email
3883 $query = 'SELECT id FROM emails WHERE emails.message_id = \''.$this->compoundMessageId.'\' and emails.deleted = 0';
3884 $r = $this->db->query($query, true);
3885 $a = $this->db->fetchByAssoc($r);
3887 $this->email = new Email();
3888 $this->email->id = $a['id'];
3897 * shiny new importOneEmail() method
3899 * @param bool forDisplay
3900 * @param clean_email boolean, default true,
3902 function importOneEmail($msgNo, $uid, $forDisplay=false, $clean_email=true) {
3903 $GLOBALS['log']->debug("InboundEmail processing 1 email {$msgNo}-----------------------------------------------------------------------------------------");
3905 global $app_strings;
3906 global $app_list_strings;
3907 global $sugar_config;
3908 global $current_user;
3911 // So, on older versions of PHP (PHP VERSION < 5.3),
3912 // calling imap_headerinfo and imap_fetchheader can cause a buffer overflow for exteremly large headers,
3913 // This leads to the remaining messages not being read because Sugar crashes everytime it tries to read the headers.
3914 // The workaround is to mark a message as read before making trying to read the header of the msg in question
3915 // This forces this message not be read again, and we can continue processing remaining msgs.
3917 // UNCOMMENT THIS IF YOU HAVE THIS PROBLEM! See notes on Bug # 45477
3918 // $this->markEmails($uid, "read");
3920 $header = imap_headerinfo($this->conn, $msgNo);
3921 $fullHeader = imap_fetchheader($this->conn, $msgNo); // raw headers
3923 // reset inline images cache
3924 $this->inlineImages = array();
3926 // handle messages deleted on server
3927 if(empty($header)) {
3928 if(!isset($this->email) || empty($this->email)) {
3929 $this->email = new Email();
3933 if ($this->isPop3Protocol()) {
3934 $this->email->name = $app_strings['LBL_EMAIL_ERROR_MESSAGE_DELETED'];
3935 $q = "DELETE FROM email_cache WHERE message_id = '{$uid}' AND ie_id = '{$this->id}' AND mbox = '{$this->mailbox}'";
3937 $this->email->name = $app_strings['LBL_EMAIL_ERROR_IMAP_MESSAGE_DELETED'];
3938 $q = "DELETE FROM email_cache WHERE imap_uid = {$uid} AND ie_id = '{$this->id}' AND mbox = '{$this->mailbox}'";
3940 // delete local cache
3941 $r = $this->db->query($q);
3943 $this->email->date_sent = $timedate->nowDb();
3945 //return "Message deleted from server.";
3948 ///////////////////////////////////////////////////////////////////////
3949 //// DUPLICATE CHECK
3950 $dupeCheckResult = $this->importDupeCheck($header->message_id, $header, $fullHeader);
3951 if($forDisplay || $dupeCheckResult) {
3952 $GLOBALS['log']->debug('*********** NO duplicate found, continuing with processing.');
3954 $structure = imap_fetchstructure($this->conn, $msgNo); // map of email
3956 ///////////////////////////////////////////////////////////////////
3957 //// CREATE SEED EMAIL OBJECT
3958 $email = new Email();
3959 $email->isDuplicate = ($dupeCheckResult) ? false : true;
3960 $email->mailbox_id = $this->id;
3962 $email->id = create_guid();
3963 $email->new_with_id = true; //forcing a GUID here to prevent double saves.
3964 //// END CREATE SEED EMAIL
3965 ///////////////////////////////////////////////////////////////////
3967 ///////////////////////////////////////////////////////////////////
3968 //// PREP SYSTEM USER
3969 if(empty($current_user)) {
3970 // I-E runs as admin, get admin prefs
3972 $current_user = new User();
3973 $current_user->getSystemUser();
3975 $tPref = $current_user->getUserDateTimePreferences();
3977 ///////////////////////////////////////////////////////////////////
3978 if(!empty($header->date)) {
3979 $unixHeaderDate = $timedate->fromString($header->date);
3981 ///////////////////////////////////////////////////////////////////
3982 //// HANDLE EMAIL ATTACHEMENTS OR HTML TEXT
3983 //// Inline images require that I-E handle attachments before body text
3984 // parts defines attachments - be mindful of .html being interpreted as an attachment
3985 if($structure->type == 1 && !empty($structure->parts)) {
3986 $GLOBALS['log']->debug('InboundEmail found multipart email - saving attachments if found.');
3987 $this->saveAttachments($msgNo, $structure->parts, $email->id, 0, $forDisplay);
3988 } elseif($structure->type == 0) {
3989 $uuemail = ($this->isUuencode($email->description)) ? true : false;
3991 * UUEncoded attachments - legacy, but still have to deal with it
3993 * begin 777 filename.txt
3998 // set body to the filtered one
4000 $email->description = $this->handleUUEncodedEmailBody($email->description, $email->id);
4001 $email->retrieve($email->id);
4005 if($this->port != 110) {
4006 $GLOBALS['log']->debug('InboundEmail found a multi-part email (id:'.$msgNo.') with no child parts to parse.');
4009 //// END HANDLE EMAIL ATTACHEMENTS OR HTML TEXT
4010 ///////////////////////////////////////////////////////////////////
4012 ///////////////////////////////////////////////////////////////////
4013 //// ASSIGN APPROPRIATE ATTRIBUTES TO NEW EMAIL OBJECT
4014 // handle UTF-8/charset encoding in the ***headers***
4016 $email->name = $this->handleMimeHeaderDecode($header->subject);
4017 $email->type = 'inbound';
4018 if(!empty($unixHeaderDate)) {
4019 $email->date_sent = $timedate->asUser($unixHeaderDate);
4020 list($email->date_start, $email->time_start) = $timedate->split_date_time($email->date_sent);
4022 $email->date_start = $email->time_start = $email->date_sent = "";
4024 $email->status = 'unread'; // this is used in Contacts' Emails SubPanel
4025 if(!empty($header->toaddress)) {
4026 $email->to_name = $this->handleMimeHeaderDecode($header->toaddress);
4027 $email->to_addrs_names = $email->to_name;
4029 if(!empty($header->to)) {
4030 $email->to_addrs = $this->convertImapToSugarEmailAddress($header->to);
4032 $email->from_name = $this->handleMimeHeaderDecode($header->fromaddress);
4033 $email->from_addr_name = $email->from_name;
4034 $email->from_addr = $this->convertImapToSugarEmailAddress($header->from);
4035 if(!empty($header->cc)) {
4036 $email->cc_addrs = $this->convertImapToSugarEmailAddress($header->cc);
4038 if(!empty($header->ccaddress)) {
4039 $email->cc_addrs_names = $this->handleMimeHeaderDecode($header->ccaddress);
4041 $email->reply_to_name = $this->handleMimeHeaderDecode($header->reply_toaddress);
4042 $email->reply_to_email = $this->convertImapToSugarEmailAddress($header->reply_to);
4043 if (!empty($email->reply_to_email)) {
4044 $email->reply_to_addr = $email->reply_to_name;
4046 $email->intent = $this->mailbox_type;
4048 $email->message_id = $this->compoundMessageId; // filled by importDupeCheck();
4050 $oldPrefix = $this->imagePrefix;
4052 // Store CIDs in imported messages, convert on display
4053 $this->imagePrefix = "cid:";
4055 // handle multi-part email bodies
4056 $email->description_html= $this->getMessageText($msgNo, 'HTML', $structure, $fullHeader,$clean_email); // runs through handleTranserEncoding() already
4057 $email->description = $this->getMessageText($msgNo, 'PLAIN', $structure, $fullHeader,$clean_email); // runs through handleTranserEncoding() already
4058 $this->imagePrefix = $oldPrefix;
4060 // empty() check for body content
4061 if(empty($email->description)) {
4062 $GLOBALS['log']->debug('InboundEmail Message (id:'.$email->message_id.') has no body');
4066 if (!empty($_REQUEST['user_id'])) {
4067 $email->assigned_user_id = $_REQUEST['user_id'];
4069 // Samir Gandhi : Commented out this code as its not needed
4070 //$email->assigned_user_id = $this->group_id;
4073 //Assign Parent Values if set
4074 if (!empty($_REQUEST['parent_id']) && !empty($_REQUEST['parent_type'])) {
4075 $email->parent_id = $_REQUEST['parent_id'];
4076 $email->parent_type = $_REQUEST['parent_type'];
4078 $mod = strtolower($email->parent_type);
4079 $rel = array_key_exists($mod, $email->field_defs) ? $mod : $mod . "_activities_emails"; //Custom modules rel name
4081 if(! $email->load_relationship($rel) )
4083 $email->$rel->add($email->parent_id);
4086 // override $forDisplay w/user pref
4088 if($this->isAutoImport()) {
4089 $forDisplay = false; // triggers save of imported email
4096 $email->new_with_id = false; // to allow future saves by UPDATE, instead of INSERT
4097 //// ASSIGN APPROPRIATE ATTRIBUTES TO NEW EMAIL OBJECT
4098 ///////////////////////////////////////////////////////////////////
4100 ///////////////////////////////////////////////////////////////////
4101 //// LINK APPROPRIATE BEANS TO NEWLY SAVED EMAIL
4102 //$contactAddr = $this->handleLinking($email);
4103 //// END LINK APPROPRIATE BEANS TO NEWLY SAVED EMAIL
4104 ///////////////////////////////////////////////////////////////////
4106 ///////////////////////////////////////////////////////////////////
4107 //// MAILBOX TYPE HANDLING
4108 $this->handleMailboxType($email, $header);
4109 //// END MAILBOX TYPE HANDLING
4110 ///////////////////////////////////////////////////////////////////
4112 ///////////////////////////////////////////////////////////////////
4113 //// SEND AUTORESPONSE
4114 if(!empty($email->reply_to_email)) {
4115 $contactAddr = $email->reply_to_email;
4117 $contactAddr = $email->from_addr;
4119 if (!$this->isMailBoxTypeCreateCase()) {
4120 $this->handleAutoresponse($email, $contactAddr);
4122 //// END SEND AUTORESPONSE
4123 ///////////////////////////////////////////////////////////////////
4124 //// END IMPORT ONE EMAIL
4125 ///////////////////////////////////////////////////////////////////
4128 // only log if not POP3; pop3 iterates through ALL mail
4129 if($this->protocol != 'pop3') {
4130 $GLOBALS['log']->info("InboundEmail found a duplicate email: ".$header->message_id);
4131 //echo "This email has already been imported";
4135 //// END DUPLICATE CHECK
4136 ///////////////////////////////////////////////////////////////////////
4138 ///////////////////////////////////////////////////////////////////////
4139 //// DEAL WITH THE MAILBOX
4141 $r = imap_setflag_full($this->conn, $msgNo, '\\SEEN');
4143 // if delete_seen, mark msg as deleted
4144 if($this->delete_seen == 1 && !$forDisplay) {
4145 $GLOBALS['log']->info("INBOUNDEMAIL: delete_seen == 1 - deleting email");
4146 imap_setflag_full($this->conn, $msgNo, '\\DELETED');
4149 // for display - don't touch server files?
4150 //imap_setflag_full($this->conn, $msgNo, '\\UNSEEN');
4153 $GLOBALS['log']->debug('********************************* InboundEmail finished import of 1 email: '.$email->name);
4154 //// END DEAL WITH THE MAILBOX
4155 ///////////////////////////////////////////////////////////////////////
4157 ///////////////////////////////////////////////////////////////////////
4158 //// TO SUPPORT EMAIL 2.0
4159 $this->email = $email;
4161 if(empty($this->email->et)) {
4162 $this->email->email2init();
4169 * figures out if a plain text email body has UUEncoded attachments
4170 * @param string string The email body
4171 * @return bool True if UUEncode is detected.
4173 function isUuencode($string) {
4174 $rx = "begin [0-9]{3} .*";
4176 $exBody = explode("\r", $string);
4177 foreach($exBody as $line) {
4178 if(preg_match("/begin [0-9]{3} .*/i", $line)) {
4187 * handles UU Encoded emails - a legacy from pre-RFC 822 which must still be supported (?)
4188 * @param string raw The raw email body
4189 * @param string id Parent email ID
4190 * @return string The filtered email body, stripped of attachments
4192 function handleUUEncodedEmailBody($raw, $id) {
4196 $attachmentBody = '';
4197 $inAttachment = false;
4199 $exRaw = explode("\n", $raw);
4201 foreach($exRaw as $k => $line) {
4202 $line = trim($line);
4204 if(preg_match("/begin [0-9]{3} .*/i", $line, $m)) {
4205 $inAttachment = true;
4206 $fileName = $this->handleEncodedFilename(substr($m[0], 10, strlen($m[0])));
4208 $attachmentBody = ''; // reset for next part of loop;
4213 if(strpos($line, "end") === 0) {
4214 if(!empty($fileName) && !empty($attachmentBody)) {
4215 $this->handleUUDecode($id, $fileName, trim($attachmentBody));
4216 $attachmentBody = ''; // reset for next part of loop;
4220 if($inAttachment === false) {
4221 $emailBody .= "\n".$line;
4223 $attachmentBody .= "\n".$line;
4227 /* 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 */
4228 $emailBody = $locale->translateCharset($emailBody, $locale->getExportCharset(), 'UTF-8');
4233 * wrapper for UUDecode
4234 * @param string id Id of the email
4235 * @param string UUEncode Encode US-ASCII
4237 function handleUUDecode($id, $fileName, $UUEncode) {
4238 global $sugar_config;
4239 /* include PHP_Compat library; it auto-feels for PHP5's compiled convert_uuencode() function */
4240 require_once('include/PHP_Compat/convert_uudecode.php');
4242 $attach = new Note();
4243 $attach->parent_id = $id;
4244 $attach->parent_type = 'Emails';
4246 $fname = $this->handleEncodedFilename($fileName);
4248 if(!empty($fname)) {//assign name to attachment
4249 $attach->name = $fname;
4250 } else {//if name is empty, default to filename
4251 $attach->name = urlencode($fileName);
4254 $attach->filename = urlencode($attach->name);
4256 //get position of last "." in file name
4257 $file_ext_beg = strrpos($attach->filename,".");
4259 //get file extension
4260 if($file_ext_beg >0) {
4261 $file_ext = substr($attach->filename, $file_ext_beg+1);
4263 //check to see if this is a file with extension located in "badext"
4264 foreach($sugar_config['upload_badext'] as $badExt) {
4265 if(strtolower($file_ext) == strtolower($badExt)) {
4266 //if found, then append with .txt and break out of lookup
4267 $attach->name = $attach->name . ".txt";
4268 $attach->file_mime_type = 'text/';
4269 $attach->filename = $attach->filename . ".txt";
4270 break; // no need to look for more
4275 $bin = convert_uudecode($UUEncode);
4276 $filename = "upload://{$attach->id}";
4277 if(file_put_contents($filename, $bin)) {
4278 $GLOBALS['log']->debug('InboundEmail saved attachment file: '.$filename);
4280 $GLOBALS['log']->debug('InboundEmail could not create attachment file: '.$filename);
4285 * returns true if the email's domain is NOT in the filter domain string
4287 * @param object email Email object in question
4288 * @return bool true if not filtered, false if filtered
4290 function checkFilterDomain($email) {
4291 $filterDomain = $this->get_stored_options('filter_domain');
4292 if(!isset($filterDomain) || empty($filterDomain)) {
4293 return true; // nothing set for this
4295 $replyTo = strtolower($email->reply_to_email);
4296 $from = strtolower($email->from_addr);
4297 $filterDomain = '@'.strtolower($filterDomain);
4298 if(strpos($replyTo, $filterDomain) !== false) {
4299 $GLOBALS['log']->debug('Autoreply cancelled - [reply to] address domain matches filter domain.');
4301 } elseif(strpos($from, $filterDomain) !== false) {
4302 $GLOBALS['log']->debug('Autoreply cancelled - [from] address domain matches filter domain.');
4305 return true; // no match
4311 * returns true if subject is NOT "out of the office" type
4313 * @param string subject Subject line of email in question
4314 * @return bool returns false if OOTO found
4316 function checkOutOfOffice($subject) {
4317 $ooto = array("Out of the Office", "Out of Office");
4319 foreach($ooto as $str) {
4320 if(preg_match('/'.$str.'/i', $subject)) {
4321 $GLOBALS['log']->debug('Autoreply cancelled - found "Out of Office" type of subject.');
4325 return true; // no matches to ooto strings
4330 * sets a timestamp for an autoreply to a single email addy
4332 * @param string addr Address of auto-replied target
4334 function setAutoreplyStatus($addr) {
4335 $timedate = TimeDate::getInstance();
4336 $this->db->query( 'INSERT INTO inbound_email_autoreply (id, deleted, date_entered, date_modified, autoreplied_to, ie_id) VALUES (
4337 \''.create_guid().'\',
4339 \''.$timedate->nowDb().'\',
4340 \''.$timedate->nowDb().'\',
4342 \''.$this->id.'\') ', true);
4347 * returns true if recipient has NOT received 10 auto-replies in 24 hours
4349 * @param string from target address for auto-reply
4350 * @return bool true if target is valid/under limit
4352 function getAutoreplyStatus($from) {
4353 global $sugar_config;
4354 $timedate = TimeDate::getInstance();
4356 $q_clean = 'UPDATE inbound_email_autoreply SET deleted = 1 WHERE date_entered < \''.$timedate->getNow()->modify("-24 hours")->asDb().'\'';
4357 $r_clean = $this->db->query($q_clean, true);
4359 $q = 'SELECT count(*) AS c FROM inbound_email_autoreply WHERE deleted = 0 AND autoreplied_to = \''.$from.'\' AND ie_id = \''.$this->id.'\'';
4360 $r = $this->db->query($q, true);
4361 $a = $this->db->fetchByAssoc($r);
4363 $email_num_autoreplies_24_hours = $this->get_stored_options('email_num_autoreplies_24_hours');
4364 $maxReplies = (isset($email_num_autoreplies_24_hours)) ? $email_num_autoreplies_24_hours : $this->maxEmailNumAutoreplies24Hours;
4366 if($a['c'] >= $maxReplies) {
4367 $GLOBALS['log']->debug('Autoreply cancelled - more than ' . $maxReplies . ' replies sent in 24 hours.');
4375 * returns exactly 1 id match. if more than one, than returns false
4376 * @param $emailName the subject of the email to match
4377 * @param $tableName the table of the matching bean type
4379 function getSingularRelatedId($emailName, $tableName) {
4380 $repStrings = array('RE:','Re:','re:');
4381 $preppedName = str_replace($repStrings,'',trim($emailName));
4383 //TODO add team security to this query
4384 $q = 'SELECT count(id) AS c FROM '.$tableName.' WHERE deleted = 0 AND name LIKE \'%'.$preppedName.'%\'';
4385 $r = $this->db->query($q, true);
4386 $a = $this->db->fetchByAssoc($r);
4389 $q = 'SELECT id FROM '.$tableName.' WHERE deleted = 0 AND name LIKE \'%'.$preppedName.'%\'';
4390 $r = $this->db->query($q, true);
4391 $a = $this->db->fetchByAssoc($r);
4399 * saves InboundEmail parse macros to config.php
4400 * @param string type Bean to link
4401 * @param string macro The new macro
4403 function saveInboundEmailSystemSettings($type, $macro) {
4404 global $sugar_config;
4406 // inbound_email_case_subject_macro
4407 $var = "inbound_email_".strtolower($type)."_subject_macro";
4408 $sugar_config[$var] = $macro;
4410 ksort($sugar_config);
4412 $sugar_config_string = "<?php\n" .
4413 '// created: ' . date('Y-m-d H:i:s') . "\n" .
4414 '$sugar_config = ' .
4415 var_export($sugar_config, true) .
4418 write_array_to_file("sugar_config", $sugar_config, "config.php");
4422 * returns the HTML for InboundEmail system settings
4423 * @return string HTML
4425 function getSystemSettingsForm() {
4426 global $sugar_config;
4427 global $mod_strings;
4428 global $app_strings;
4429 global $app_list_strings;
4434 $macro = $c->getEmailSubjectMacro();
4437 <form action="index.php" method="post" name="Macro" id="form">
4438 <input type="hidden" name="module" value="InboundEmail">
4439 <input type="hidden" name="action" value="ListView">
4440 <input type="hidden" name="save" value="true">
4442 <table width="100%" cellpadding="0" cellspacing="0" border="0">
4445 <input title="{$app_strings['LBL_SAVE_BUTTON_TITLE']}"
4446 accessKey="{$app_strings['LBL_SAVE_BUTTON_KEY']}"
4448 onclick="this.form.return_module.value='InboundEmail'; this.form.return_action.value='ListView';"
4449 type="submit" name="Edit" value=" {$app_strings['LBL_SAVE_BUTTON_LABEL']} ">
4454 <table width="100%" border="0" cellspacing="0" cellpadding="0" class="detail view">
4456 <td valign="top" width='10%' NOWRAP scope="row">
4458 <b>{$mod_strings['LBL_CASE_MACRO']}:</b>
4461 <td valign="top" width='20%'>
4463 <input name="inbound_email_case_macro" type="text" value="{$macro}">
4466 <td valign="top" width='70%'>
4468 {$mod_strings['LBL_CASE_MACRO_DESC']}
4470 <i>{$mod_strings['LBL_CASE_MACRO_DESC2']}</i>
4481 * For mailboxes of type "Support" parse for '[CASE:%1]'
4483 * @param string $emailName The subject line of the email
4484 * @param aCase $aCase A Case object
4486 * @return string|boolean Case ID or FALSE if not found
4488 function getCaseIdFromCaseNumber($emailName, $aCase) {
4489 //$emailSubjectMacro
4490 $exMacro = explode('%1', $aCase->getEmailSubjectMacro());
4491 $open = $exMacro[0];
4492 $close = $exMacro[1];
4494 if($sub = stristr($emailName, $open)) { // eliminate everything up to the beginning of the macro and return the rest
4495 // $sub is [CASE:XX] xxxxxxxxxxxxxxxxxxxxxx
4496 $sub2 = str_replace($open, '', $sub);
4497 // $sub2 is XX] xxxxxxxxxxxxxx
4498 $sub3 = substr($sub2, 0, strpos($sub2, $close));
4500 // case number is supposed to be numeric
4501 if (ctype_digit($sub3)) {
4502 // filter out deleted records in order to create a new case
4503 // if email is related to deleted one (bug #49840)
4504 $query = 'SELECT id FROM cases WHERE case_number = '
4505 . $this->db->quoted($sub3)
4506 . ' and deleted = 0';
4507 $r = $this->db->query($query, true);
4508 $a = $this->db->fetchByAssoc($r);
4509 if (!empty($a['id'])) {
4518 function get_stored_options($option_name,$default_value=null,$stored_options=null) {
4519 if (empty($stored_options)) {
4520 $stored_options=$this->stored_options;
4522 if(!empty($stored_options)) {
4523 $storedOptions = unserialize(base64_decode($stored_options));
4524 if (isset($storedOptions[$option_name])) {
4525 $default_value=$storedOptions[$option_name];
4528 return $default_value;
4533 * This function returns a contact or user ID if a matching email is found
4534 * @param $email the email address to match
4535 * @param $table which table to query
4537 function getRelatedId($email, $module) {
4538 $email = trim(strtoupper($email));
4539 if(strpos($email, ',') !== false) {
4540 $emailsArray = explode(',', $email);
4541 $emailAddressString = "";
4542 foreach($emailsArray as $emailAddress) {
4543 if (!empty($emailAddressString)) {
4544 $emailAddressString .= ",";
4546 $emailAddressString .= $this->db->quoted(trim($emailAddress));
4548 $email = $emailAddressString;
4550 $email = $this->db->quoted($email);
4552 $module = $this->db->quoted(ucfirst($module));
4554 $q = "SELECT bean_id FROM email_addr_bean_rel eabr
4555 JOIN email_addresses ea ON (eabr.email_address_id = ea.id)
4556 WHERE bean_module = $module AND ea.email_address_caps in ( {$email} ) AND eabr.deleted=0";
4558 $r = $this->db->query($q, true);
4561 while($a = $this->db->fetchByAssoc($r)) {
4562 $retArr[] = $a['bean_id'];
4564 if(count($retArr) > 0) {
4572 * finds emails tagged "//UNSEEN" on mailserver and "SINCE: [date]" if that
4575 * @return array Array of messageNumbers (mail server's internal keys)
4577 function getNewMessageIds() {
4578 $storedOptions = unserialize(base64_decode($this->stored_options));
4580 //TODO figure out if the since date is UDT
4581 if($storedOptions['only_since']) {// POP3 does not support Unseen flags
4582 if(!isset($storedOptions['only_since_last']) && !empty($storedOptions['only_since_last'])) {
4583 $q = 'SELECT last_run FROM schedulers WHERE job = \'function::pollMonitoredInboxes\'';
4584 $r = $this->db->query($q, true);
4585 $a = $this->db->fetchByAssoc($r);
4587 $date = date('r', strtotime($a['last_run']));
4589 $date = $storedOptions['only_since_last'];
4591 $ret = imap_search($this->conn, 'SINCE "'.$date.'" UNDELETED UNSEEN');
4592 $check = imap_check($this->conn);
4593 $storedOptions['only_since_last'] = $check->Date;
4594 $this->stored_options = base64_encode(serialize($storedOptions));
4597 $ret = imap_search($this->conn, 'UNDELETED UNSEEN');
4600 $GLOBALS['log']->debug('-----> getNewMessageIds() got '.count($ret).' new Messages');
4605 * Constructs the resource connection string that IMAP needs
4606 * @param string $service Service string, will generate if not passed
4609 function getConnectString($service='', $mbox='', $includeMbox=true) {
4610 $service = empty($service) ? $this->getServiceString() : $service;
4611 $mbox = empty($mbox) ? $this->mailbox : $mbox;
4613 $connectString = '{'.$this->server_url.':'.$this->port.'/service='.$this->protocol.$service.'}';
4614 $connectString .= ($includeMbox) ? $mbox : "";
4616 return $connectString;
4619 function disconnectMailserver() {
4620 if(is_resource($this->conn)) {
4621 imap_close($this->conn);
4626 * Connects to mailserver. If an existing IMAP resource is available, it
4627 * will attempt to reuse the connection, updating the mailbox path.
4629 * @param bool test Flag to test connection
4630 * @param bool force Force reconnect
4631 * @return string "true" on success, "false" or $errorMessage on failure
4633 function connectMailserver($test=false, $force=false) {
4634 global $mod_strings;
4635 if(!function_exists("imap_open")) {
4636 $GLOBALS['log']->debug('------------------------- IMAP libraries NOT available!!!! die()ing thread.----');
4637 return $mod_strings['LBL_WARN_NO_IMAP'];
4640 imap_errors(); // clearing error stack
4641 error_reporting(0); // turn off notices from IMAP
4643 // tls::ca::ssl::protocol::novalidate-cert::notls
4644 $useSsl = ($_REQUEST['ssl'] == 'true') ? true : false;
4646 imap_timeout(1, 15); // 60 secs is the default
4647 imap_timeout(2, 15);
4648 imap_timeout(3, 15);
4650 $opts = $this->findOptimumSettings($useSsl);
4651 if(isset($opts['good']) && empty($opts['good'])) {
4652 return array_pop($opts['err']);
4654 $service = $opts['service'];
4655 $service = str_replace('foo','', $service); // foo there to support no-item explodes
4658 $service = $this->getServiceString();
4661 $connectString = $this->getConnectString($service, $this->mailbox);
4664 * Try to recycle the current connection to reduce response times
4666 if(is_resource($this->conn)) {
4669 imap_close($this->conn);
4672 if(imap_ping($this->conn)) {
4673 // we have a live connection
4674 imap_reopen($this->conn, $connectString, CL_EXPUNGE);
4679 if(!is_resource($this->conn) && !$test) {
4680 $this->conn = $this->getImapConnection($connectString, $this->email_user, $this->email_password, CL_EXPUNGE);
4684 if ($opts == false && !is_resource($this->conn)) {
4685 $this->conn = $this->getImapConnection($connectString, $this->email_user, $this->email_password, CL_EXPUNGE);
4689 $successful = false;
4690 if(($errors = imap_last_error()) || ($alerts = imap_alerts())) {
4691 if($errors == 'Mailbox is empty') { // false positive
4695 $msg .= '<p>'.$alerts.'<p>';
4696 $msg .= '<p>'.$mod_strings['ERR_TEST_MAILBOX'];
4703 if($this->protocol == 'imap') {
4704 $msg .= $mod_strings['LBL_TEST_SUCCESSFUL'];
4706 $testConnectString = '{'.$this->server_url.':'.$this->port.'/service='.$this->protocol.$service.'}';
4707 if (!is_resource($this->conn)) {
4708 $this->conn = imap_open($connectString, $this->email_user, $this->email_password, CL_EXPUNGE);
4710 $list = imap_getmailboxes($this->conn, $testConnectString, "*");
4711 if(isset($_REQUEST['personal']) && $_REQUEST['personal'] == 'true') {
4712 $msg .= $mod_strings['LBL_TEST_SUCCESSFUL'];
4713 } elseif (is_array($list)) {
4717 $msg .= '<b>'.$mod_strings['LBL_FOUND_MAILBOXES'].'</b><p>';
4718 foreach ($list as $key => $val) {
4719 $mb = imap_utf7_decode(str_replace($testConnectString,'',$val->name));
4720 $msg .= '<a onClick=\'setMailbox(\"'.$mb.'\"); window.close();\'>';
4726 $msg .= '<p>'.$mod_strings['ERR_MAILBOX_FAIL'].imap_last_error().'</p>';
4727 $msg .= '<p>'.$mod_strings['ERR_TEST_MAILBOX'].'</p>';
4731 $msg .= $mod_strings['LBL_POP3_SUCCESS'];
4735 imap_errors(); // collapse error stack
4736 imap_close($this->conn);
4738 } elseif(!is_resource($this->conn)) {
4739 $GLOBALS['log']->info('Couldn\'t connect to mail server id: ' . $this->id);
4742 $GLOBALS['log']->info('Connected to mail server id: ' . $this->id);
4749 function checkImap() {
4750 global $mod_strings;
4752 if(!function_exists('imap_open')) {
4754 <table cellpadding="0" cellspacing="0" width="100%" border="0" class="list view">
4756 <td scope="col" width="25%" colspan="2"><slot>
4757 '.$mod_strings['LBL_WARN_IMAP_TITLE'].'
4761 <td scope="row" valign=TOP bgcolor="#fdfdfd" width="20%"><slot>
4762 '.$mod_strings['LBL_WARN_IMAP'].'
4763 <td scope="row" valign=TOP class="oddListRowS1" bgcolor="#fdfdfd" width="80%"><slot>
4764 <span class=error>'.$mod_strings['LBL_WARN_NO_IMAP'].'</span>
4773 * Attempt to create an IMAP connection using passed in parameters
4774 * return either the connection resource or false if unable to connect
4776 * @param string $mailbox Mailbox to be used to create imap connection
4777 * @param string $username The user name
4778 * @param string $password The password associated with the username
4779 * @param integer $options Bitmask for options parameter to the imap_open function
4781 * @return resource|boolean Connection resource on success, FALSE on failure
4783 protected function getImapConnection($mailbox, $username, $password, $options = 0)
4785 // if php is prior to 5.3.2, then return call without disable parameters as they are not supported yet
4786 if (version_compare(phpversion(), '5.3.2', '<')) {
4787 return imap_open($mailbox, $username, $password, $options);
4791 $authenticators = array('', 'GSSAPI', 'NTLM');
4793 while (!$connection && ($authenticator = array_shift($authenticators)) !== null) {
4794 if ($authenticator) {
4796 'DISABLE_AUTHENTICATOR' => $authenticator,
4802 $connection = imap_open($mailbox, $username, $password, $options, 0, $params);
4809 * retrieves an array of I-E beans based on the group_id
4810 * @param string $groupId GUID of the group user or Individual
4811 * @return array $beans array of beans
4812 * @return boolean false if none returned
4814 function retrieveByGroupId($groupId) {
4815 $q = 'SELECT id FROM inbound_email WHERE group_id = \''.$groupId.'\' AND deleted = 0 AND status = \'Active\'';
4816 $r = $this->db->query($q, true);
4819 while($a = $this->db->fetchByAssoc($r)) {
4820 $ie = new InboundEmail();
4821 $ie->retrieve($a['id']);
4822 $beans[$a['id']] = $ie;
4828 * Retrieves the current count of personal accounts for the user specified.
4830 * @param unknown_type $user
4832 function getUserPersonalAccountCount($user = null)
4835 $user = $GLOBALS['current_user'];
4837 $query = "SELECT count(*) as c FROM inbound_email WHERE deleted=0 AND is_personal='1' AND group_id='{$user->id}' AND status='Active'";
4839 $rs = $this->db->query($query);
4840 $row = $this->db->fetchByAssoc($rs);
4845 * retrieves an array of I-E beans based on the group folder id
4846 * @param string $groupFolderId GUID of the group folder
4847 * @return array $beans array of beans
4848 * @return boolean false if none returned
4850 function retrieveByGroupFolderId($groupFolderId) {
4851 $q = 'SELECT id FROM inbound_email WHERE groupfolder_id = \''.$groupFolderId.'\' AND deleted = 0 ';
4852 $r = $this->db->query($q, true);
4855 while($a = $this->db->fetchByAssoc($r)) {
4856 $ie = new InboundEmail();
4857 $ie->retrieve($a['id']);
4864 * Retrieves an array of I-E beans that the user has team access to
4866 function retrieveAllByGroupId($id, $includePersonal=true) {
4867 global $current_user;
4869 $beans = ($includePersonal) ? $this->retrieveByGroupId($id) : array();
4875 // bug 50536: groupfolder_id cannot be updated to NULL from sugarbean's nullable check ('type' set to ID in the vardef)
4876 // hence the awkward or check -- rbacon
4877 $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' ";
4881 $r = $this->db->query($q, true);
4883 while($a = $this->db->fetchByAssoc($r)) {
4885 foreach($beans as $bean) {
4886 if($bean->id == $a['id']) {
4892 $ie = new InboundEmail();
4893 $ie->retrieve($a['id']);
4894 $beans[$a['id']] = $ie;
4902 * Retrieves an array of I-E beans that the user has team access to including group
4904 function retrieveAllByGroupIdWithGroupAccounts($id, $includePersonal=true) {
4905 global $current_user;
4907 $beans = ($includePersonal) ? $this->retrieveByGroupId($id) : array();
4916 $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 ";
4918 $r = $this->db->query($q, true);
4920 while($a = $this->db->fetchByAssoc($r)) {
4922 foreach($beans as $bean) {
4923 if($bean->id == $a['id']) {
4929 $ie = new InboundEmail();
4930 $ie->retrieve($a['id']);
4931 $beans[$a['id']] = $ie;
4940 * returns the bean name - overrides SugarBean's
4942 function get_summary_text() {
4947 * Override's SugarBean's
4949 function create_export_query($order_by, $where, $show_deleted = 0) {
4950 return $this->create_new_list_query($order_by, $where, $show_deleted = 0);
4954 * Override's SugarBean's
4958 * Override's SugarBean's
4960 function get_list_view_data(){
4961 global $mod_strings;
4962 global $app_list_strings;
4963 $temp_array = $this->get_list_view_array();
4964 $temp_array['MAILBOX_TYPE_NAME']= $app_list_strings['dom_mailbox_type'][$this->mailbox_type];
4965 //cma, fix bug 21670.
4966 $temp_array['GLOBAL_PERSONAL_STRING']= ($this->is_personal ? $mod_strings['LBL_IS_PERSONAL'] : $mod_strings['LBL_IS_GROUP']);
4967 $temp_array['STATUS'] = ($this->status == 'Active') ? $mod_strings['LBL_STATUS_ACTIVE'] : $mod_strings['LBL_STATUS_INACTIVE'];
4972 * Override's SugarBean's
4974 function fill_in_additional_list_fields() {
4975 $this->fill_in_additional_detail_fields();
4979 * Override's SugarBean's
4981 function fill_in_additional_detail_fields() {
4982 if(!empty($this->service)) {
4983 $exServ = explode('::', $this->service);
4984 $this->tls = $exServ[0];
4985 if ( isset($exServ[1]) )
4986 $this->ca = $exServ[1];
4987 if ( isset($exServ[2]) )
4988 $this->ssl = $exServ[2];
4989 if ( isset($exServ[3]) )
4990 $this->protocol = $exServ[3];
5007 ///////////////////////////////////////////////////////////////////////////
5008 //// IN SUPPORT OF EMAIL 2.0
5010 * Checks for $user's autoImport setting and returns the current value
5011 * @param object $user User in focus, defaults to $current_user
5014 function isAutoImport($user=null) {
5015 if(!empty($this->autoImport)) {
5016 return $this->autoImport;
5019 global $current_user;
5020 if(empty($user)) $user = $current_user;
5022 $emailSettings = $current_user->getPreference('emailSettings', 'Emails');
5023 $emailSettings = is_string($emailSettings) ? unserialize($emailSettings) : $emailSettings;
5025 $this->autoImport = (isset($emailSettings['autoImport']) && !empty($emailSettings['autoImport'])) ? true : false;
5026 return $this->autoImport;
5030 * Clears out cache files for a user
5032 function cleanOutCache() {
5033 $GLOBALS['log']->debug("INBOUNDEMAIL: at cleanOutCache()");
5034 $this->deleteCache();
5038 * moves emails from folder to folder
5039 * @param string $fromIe I-E id
5040 * @param string $fromFolder IMAP path to folder in which the email lives
5041 * @param string $toIe I-E id
5042 * @param string $toFolder
5043 * @param string $uids UIDs of emails to move, either Sugar GUIDS or IMAP
5046 function copyEmails($fromIe, $fromFolder, $toIe, $toFolder, $uids) {
5047 $this->moveEmails($fromIe, $fromFolder, $toIe, $toFolder, $uids, true);
5051 * moves emails from folder to folder
5052 * @param string $fromIe I-E id
5053 * @param string $fromFolder IMAP path to folder in which the email lives
5054 * @param string $toIe I-E id
5055 * @param string $toFolder
5056 * @param string $uids UIDs of emails to move, either Sugar GUIDS or IMAP
5058 * @param bool $copy Default false
5059 * @return bool True on successful execution
5061 function moveEmails($fromIe, $fromFolder, $toIe, $toFolder, $uids, $copy=false) {
5062 global $app_strings;
5063 global $current_user;
5067 if($fromIe == $toIe) {
5068 $GLOBALS['log']->debug("********* SUGARFOLDER - moveEmails() moving email from I-E to I-E");
5069 //$exDestFolder = explode("::", $toFolder);
5070 //preserve $this->mailbox
5071 if (isset($this->mailbox)) {
5072 $oldMailbox = $this->mailbox;
5076 $this->retrieve($fromIe);
5077 $this->mailbox = $fromFolder;
5078 $this->connectMailserver();
5079 $exUids = explode('::;::', $uids);
5080 $uids = implode(",", $exUids);
5081 // imap_mail_move accepts comma-delimited lists of UIDs
5083 if(imap_mail_copy($this->conn, $uids, $toFolder, CP_UID)) {
5084 $this->mailbox = $toFolder;
5085 $this->connectMailserver();
5086 $newOverviews = imap_fetch_overview($this->conn, $uids, FT_UID);
5087 $this->updateOverviewCacheFile($newOverviews, 'append');
5088 if (isset($oldMailbox)) {
5089 $this->mailbox = $oldMailbox;
5093 $GLOBALS['log']->debug("INBOUNDEMAIL: could not imap_mail_copy() [ {$uids} ] to folder [ {$toFolder} ] from folder [ {$fromFolder} ]");
5096 if(imap_mail_move($this->conn, $uids, $toFolder, CP_UID)) {
5097 $GLOBALS['log']->info("INBOUNDEMAIL: imap_mail_move() [ {$uids} ] to folder [ {$toFolder} ] from folder [ {$fromFolder} ]");
5098 imap_expunge($this->conn); // hard deletes moved messages
5100 // update cache on fromFolder
5101 $newOverviews = $this->getOverviewsFromCacheFile($uids, $fromFolder, true);
5102 $this->deleteCachedMessages($uids, $fromFolder);
5104 // update cache on toFolder
5105 $this->checkEmailOneMailbox($toFolder, true, true);
5106 if (isset($oldMailbox)) {
5107 $this->mailbox = $oldMailbox;
5112 $GLOBALS['log']->debug("INBOUNDEMAIL: could not imap_mail_move() [ {$uids} ] to folder [ {$toFolder} ] from folder [ {$fromFolder} ]");
5115 } elseif($toIe == 'folder' && $fromFolder == 'sugar::Emails') {
5116 $GLOBALS['log']->debug("********* SUGARFOLDER - moveEmails() moving email from SugarFolder to SugarFolder");
5117 // move from sugar folder to sugar folder
5118 require_once("include/SugarFolders/SugarFolders.php");
5119 $sugarFolder = new SugarFolder();
5120 $exUids = explode($app_strings['LBL_EMAIL_DELIMITER'], $uids);
5121 foreach($exUids as $id) {
5123 $sugarFolder->copyBean($fromIe, $toFolder, $id, "Emails");
5125 $fromSugarFolder = new SugarFolder();
5126 $fromSugarFolder->retrieve($fromIe);
5127 $toSugarFolder = new SugarFolder();
5128 $toSugarFolder->retrieve($toFolder);
5130 $email = new Email();
5131 $email->retrieve($id);
5132 $email->status = 'unread';
5134 // when you move from My Emails to Group Folder, Assign To field for the Email should become null
5135 if ($fromSugarFolder->is_dynamic && $toSugarFolder->is_group) {
5136 // Bug 50972 - assigned_user_id set to empty string not true null
5137 // Modifying the field defs in just this one place to allow
5138 // a true null since this is what is expected when reading
5140 $email->setFieldNullable('assigned_user_id');
5141 $email->assigned_user_id = "";
5143 $email->revertFieldNullable('assigned_user_id');
5145 if (!$toSugarFolder->checkEmailExistForFolder($id)) {
5146 $fromSugarFolder->deleteEmailFromAllFolder($id);
5147 $toSugarFolder->addBean($email);
5149 } elseif ($fromSugarFolder->is_group && $toSugarFolder->is_dynamic) {
5150 $fromSugarFolder->deleteEmailFromAllFolder($id);
5151 $email->assigned_user_id = $current_user->id;
5154 // If you are moving something from personal folder then delete an entry from all folder
5155 if (!$fromSugarFolder->is_dynamic && !$fromSugarFolder->is_group) {
5156 $fromSugarFolder->deleteEmailFromAllFolder($id);
5159 if ($fromSugarFolder->is_dynamic && !$toSugarFolder->is_dynamic && !$toSugarFolder->is_group) {
5160 $email->assigned_user_id = "";
5161 $toSugarFolder->addBean($email);
5163 if (!$toSugarFolder->checkEmailExistForFolder($id)) {
5164 if (!$toSugarFolder->is_dynamic) {
5165 $fromSugarFolder->deleteEmailFromAllFolder($id);
5166 $toSugarFolder->addBean($email);
5168 $fromSugarFolder->deleteEmailFromAllFolder($id);
5169 $email->assigned_user_id = $current_user->id;
5172 $sugarFolder->move($fromIe, $toFolder, $id);
5180 } elseif($toIe == 'folder') {
5181 $GLOBALS['log']->debug("********* SUGARFOLDER - moveEmails() moving email from I-E to SugarFolder");
5182 // move to Sugar folder
5183 require_once("include/SugarFolders/SugarFolders.php");
5184 $sugarFolder = new SugarFolder();
5185 $sugarFolder->retrieve($toFolder);
5186 //Show the import form if we don't have the required info
5187 if (!isset($_REQUEST['delete'])) {
5188 $json = getJSONobj();
5189 if ($sugarFolder->is_group) {
5190 $_REQUEST['showTeam'] = false;
5191 $_REQUEST['showAssignTo'] = false;
5193 $ret = $this->email->et->getImportForm($_REQUEST, $this->email);
5194 $ret['move'] = true;
5195 $ret['srcFolder'] = $fromFolder;
5196 $ret['srcIeId'] = $fromIe;
5197 $ret['dstFolder'] = $toFolder;
5198 $ret['dstIeId'] = $toIe;
5199 $out = trim($json->encode($ret, false));
5206 $this->retrieve($fromIe);
5207 $this->mailbox = $fromFolder;
5208 $this->connectMailserver();
5209 // If its a group folder the team should be of the folder team
5210 if ($sugarFolder->is_group) {
5211 $_REQUEST['team_id'] = $sugarFolder->team_id;
5212 $_REQUEST['team_set_id'] = $sugarFolder->team_set_id;
5214 // TODO - set team_id, team_set for new UI
5217 $exUids = explode($app_strings['LBL_EMAIL_DELIMITER'], $uids);
5219 if(!empty($sugarFolder->id)) {
5222 $json = getJSONobj();
5223 foreach($exUids as $k => $uid) {
5225 if ($this->isPop3Protocol()) {
5226 $msgNo = $this->getCorrectMessageNoForPop3($uid);
5228 $msgNo = imap_msgno($this->conn, $uid);
5231 if(!empty($msgNo)) {
5232 $importStatus = $this->importOneEmail($msgNo, $uid);
5235 $sugarFolder->addBean($this->email);
5236 if(!$copy && isset($_REQUEST['delete']) && ($_REQUEST['delete'] == "true") && $importStatus) {
5237 $GLOBALS['log']->error("********* delete from mailserver [ {explode(",", $uids)} ]");
5238 // delete from mailserver
5239 $this->deleteMessageOnMailServer($uid);
5240 $this->deleteMessageFromCache($uid);
5243 $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']);
5247 echo $json->encode($return);
5250 $GLOBALS['log']->error("********* SUGARFOLDER - failed to retrieve folder ID [ {$toFolder} ]");
5253 $GLOBALS['log']->debug("********* SUGARFOLDER - moveEmails() called with no passing criteria");
5261 * Hard deletes an I-E account
5262 * @param string id GUID
5264 function hardDelete($id) {
5265 $q = "DELETE FROM inbound_email WHERE id = '{$id}'";
5266 $r = $this->db->query($q, true);
5270 * Generate a unique filename for attachments based on the message id. There are no maximum
5271 * specifications for the length of the message id, the only requirement is that it be globally unique.
5273 * @param bool $nameOnly Whether or not the attachment count should be appended to the filename.
5274 * @return string The temp file name
5276 function getTempFilename($nameOnly=false) {
5278 $str = $this->compoundMessageId;
5281 $str = $str.$this->attachmentCount;
5282 $this->attachmentCount++;
5289 * deletes and expunges emails on server
5290 * @param string $uid UID(s), comma delimited, of email(s) on server
5291 * @return bool true on success
5293 function deleteMessageOnMailServer($uid) {
5294 global $app_strings;
5295 $this->connectMailserver();
5297 if(strpos($uid, $app_strings['LBL_EMAIL_DELIMITER']) !== false) {
5298 $uids = explode($app_strings['LBL_EMAIL_DELIMITER'], $uid);
5305 if($this->protocol == 'imap') {
5306 $trashFolder = $this->get_stored_options("trashFolder");
5307 if (empty($trashFolder)) {
5308 $trashFolder = "INBOX.Trash";
5310 $uidsToMove = implode('::;::', $uids);
5311 if($this->moveEmails($this->id, $this->mailbox, $this->id, $trashFolder, $uidsToMove))
5312 $GLOBALS['log']->debug("INBOUNDEMAIL: MoveEmail to {$trashFolder} successful.");
5314 $GLOBALS['log']->debug("INBOUNDEMAIL: MoveEmail to {$trashFolder} FAILED - trying hard delete for message: $uid");
5315 $uidsToDelete = implode(',', $uids);
5316 imap_delete($this->conn, $uidsToDelete, FT_UID);
5322 foreach($uids as $uid) {
5323 $msgnos[] = $this->getCorrectMessageNoForPop3($uid);
5325 $msgnos = implode(',', $msgnos);
5326 imap_delete($this->conn, $msgnos);
5330 if(!imap_expunge($this->conn)) {
5331 $GLOBALS['log']->debug("NOOP: could not expunge deleted email.");
5335 $GLOBALS['log']->info("INBOUNDEMAIL: hard-deleted mail with MSgno's' [ {$msgnos} ]");
5341 * deletes and expunges emails on server
5342 * @param string $uid UID(s), comma delimited, of email(s) on server
5344 function deleteMessageOnMailServerForPop3($uid) {
5345 if(imap_delete($this->conn, $uid)) {
5346 if(!imap_expunge($this->conn)) {
5347 $GLOBALS['log']->debug("NOOP: could not expunge deleted email.");
5350 $GLOBALS['log']->info("INBOUNDEMAIL: hard-deleted mail with MSgno's' [ {$uid} ]");
5356 * Checks if this is a pop3 type of an account or not
5359 function isPop3Protocol() {
5360 return ($this->protocol == 'pop3');
5364 * Gets the UIDL from database for the corresponding msgno
5365 * @param int messageNo of a message
5366 * @return UIDL for the message
5368 function getUIDLForMessage($msgNo) {
5369 $query = "SELECT message_id FROM email_cache WHERE ie_id = '{$this->id}' AND msgno = '{$msgNo}'";
5370 $r = $this->db->query($query);
5371 $a = $this->db->fetchByAssoc($r);
5372 return $a['message_id'];
5375 * Get the users default IE account id
5380 function getUsersDefaultOutboundServerId($user)
5382 $id = $user->getPreference($this->keyForUsersDefaultIEAccount,'Emails',$user);
5383 //If no preference has been set, grab the default system id.
5386 $oe = new OutboundEmail();
5387 $system = $oe->getSystemMailerSettings();
5388 $id=empty($system->id) ? '' : $system->id;
5395 * Get the users default IE account id
5399 function setUsersDefaultOutboundServerId($user,$oe_id)
5401 $user->setPreference($this->keyForUsersDefaultIEAccount, $oe_id, '', 'Emails');
5404 * Gets the UIDL from database for the corresponding msgno
5405 * @param int messageNo of a message
5406 * @return UIDL for the message
5408 function getMsgnoForMessageID($messageid) {
5409 $query = "SELECT msgno FROM email_cache WHERE ie_id = '{$this->id}' AND message_id = '{$messageid}'";
5410 $r = $this->db->query($query);
5411 $a = $this->db->fetchByAssoc($r);
5412 return $a['message_id'];
5416 * fills InboundEmail->email with an email's details
5417 * @param int uid Unique ID of email
5418 * @param bool isMsgNo flag that passed ID is msgNo, default false
5419 * @param bool setRead Sets the 'seen' flag in cache
5420 * @param bool forceRefresh Skips cache file
5423 function setEmailForDisplay($uid, $isMsgNo=false, $setRead=false, $forceRefresh=false) {
5426 $GLOBALS['log']->debug("*** ERROR: INBOUNDEMAIL trying to setEmailForDisplay() with no UID");
5430 global $sugar_config;
5431 global $app_strings;
5433 // if its a pop3 then get the UIDL and see if this file name exist or not
5434 if ($this->isPop3Protocol()) {
5435 // get the UIDL from database;
5436 $cachedUIDL = md5($uid);
5437 $cache = "{$this->EmailCachePath}/{$this->id}/messages/{$this->mailbox}{$cachedUIDL}.php";
5439 $cache = "{$this->EmailCachePath}/{$this->id}/messages/{$this->mailbox}{$uid}.php";
5442 if(file_exists($cache) && !$forceRefresh) {
5443 $GLOBALS['log']->info("INBOUNDEMAIL: Using cache file for setEmailForDisplay()");
5445 include($cache); // profides $cacheFile
5446 /** @var $cacheFile array */
5448 $metaOut = unserialize($cacheFile['out']);
5449 $meta = $metaOut['meta']['email'];
5450 $email = new Email();
5452 foreach($meta as $k => $v) {
5456 $email->to_addrs = $meta['toaddrs'];
5457 $email->date_sent = $meta['date_start'];
5458 //_ppf($email,true);
5460 $this->email = $email;
5461 $this->email->email2init();
5464 $GLOBALS['log']->info("INBOUNDEMAIL: opening new connection for setEmailForDisplay()");
5465 if($this->isPop3Protocol()) {
5466 $msgNo = $this->getCorrectMessageNoForPop3($uid);
5468 if(empty($this->conn)) {
5469 $this->connectMailserver();
5471 $msgNo = ($isMsgNo) ? $uid : imap_msgno($this->conn, $uid);
5473 if(empty($this->conn)) {
5474 $status = $this->connectMailserver();
5475 if($status == "false") {
5476 $this->email = new Email();
5477 $this->email->name = $app_strings['LBL_EMAIL_ERROR_MAILSERVERCONNECTION'];
5484 $this->importOneEmail($msgNo, $uid, true);
5485 $this->email->id = '';
5486 $this->email->new_with_id = false;
5491 $this->setStatuses($uid, 'seen', 1);
5499 * Sets status for a particular attribute on the mailserver and the local cache file
5501 function setStatuses($uid, $field, $value) {
5502 global $sugar_config;
5503 /** available status fields
5507 [date] => Mon, 22 Jan 2007 17:32:57 -0800
5520 $file = "{$this->mailbox}.imapFetchOverview.php";
5521 $overviews = $this->getCacheValueForUIDs($this->mailbox, array($uid));
5523 if(!empty($overviews)) {
5526 foreach($overviews['retArr'] as $k => $obj) {
5527 if($obj->imap_uid == $uid) {
5528 $obj->$field = $value;
5533 if(!empty($updates)) {
5534 $this->setCacheValue($this->mailbox, array(), $updates);
5540 * Removes an email from the cache file, deletes the message from the cache too
5541 * @param string String of uids, comma delimited
5543 function deleteMessageFromCache($uids) {
5544 global $sugar_config;
5545 global $app_strings;
5547 // delete message cache file and email_cache file
5548 $exUids = explode($app_strings['LBL_EMAIL_DELIMITER'], $uids);
5550 foreach($exUids as $uid) {
5552 if ($this->isPop3Protocol()) {
5553 $q = "DELETE FROM email_cache WHERE message_id = '{$uid}' AND ie_id = '{$this->id}'";
5555 $q = "DELETE FROM email_cache WHERE imap_uid = {$uid} AND ie_id = '{$this->id}'";
5557 $r = $this->db->query($q);
5558 if ($this->isPop3Protocol()) {
5561 $msgCacheFile = "{$this->EmailCachePath}/{$this->id}/messages/{$this->mailbox}{$uid}.php";
5562 if(file_exists($msgCacheFile)) {
5563 if(!unlink($msgCacheFile)) {
5564 $GLOBALS['log']->error("***ERROR: InboundEmail could not delete the cache file [ {$msgCacheFile} ]");
5573 * @param int uid UID of email to display
5574 * @param string mbox Mailbox to look in for the message
5575 * @param bool isMsgNo Flag to assume $uid is a MessageNo, not UniqueID, default false
5577 function displayOneEmail($uid, $mbox, $isMsgNo=false) {
5578 require_once("include/JSON.php");
5581 global $app_strings;
5582 global $app_list_strings;
5583 global $sugar_smarty;
5585 global $current_user;
5586 global $sugar_config;
5588 $fetchedAttributes = array(
5597 $souEmail = array();
5598 foreach($fetchedAttributes as $k) {
5599 if ($k == 'date_start') {
5600 $this->email->$k . " " . $this->email->time_start;
5601 $souEmail[$k] = $this->email->$k . " " . $this->email->time_start;
5602 } elseif ($k == 'time_start') {
5605 $souEmail[$k] = trim($this->email->$k);
5609 // if a MsgNo is passed in, convert to UID
5611 $uid = imap_uid($this->conn, $uid);
5613 // meta object to allow quick retrieval for replies
5615 $meta['type'] = $this->email->type;
5616 $meta['uid'] = $uid;
5617 $meta['ieId'] = $this->id;
5618 $meta['email'] = $souEmail;
5619 $meta['mbox'] = $this->mailbox;
5624 $exMbox = explode("::", $mbox);
5628 if(!empty($this->email->cc_addrs)) {
5629 //$ccs = $this->collapseLongMailingList($this->email->cc_addrs);
5630 $ccs = to_html($this->email->cc_addrs_names);
5633 <td NOWRAP valign="top" class="displayEmailLabel">
5634 {$app_strings['LBL_EMAIL_CC']}:
5636 <td class="displayEmailValue">
5643 $meta['email']['cc_addrs'] = $ccs;
5646 if ($mbox == "sugar::Emails") {
5648 $q = "SELECT id, filename, file_mime_type FROM notes WHERE parent_id = '{$uid}' AND deleted = 0";
5649 $r = $this->db->query($q);
5651 while($a = $this->db->fetchByAssoc($r)) {
5652 $url = "index.php?entryPoint=download&type=notes&id={$a['id']}";
5653 $lbl = ($i == 0) ? $app_strings['LBL_EMAIL_ATTACHMENTS'].":" : '';
5655 $attachments .=<<<EOQ
5657 <td NOWRAP valign="top" class="displayEmailLabel">
5660 <td NOWRAP valign="top" colspan="2" class="displayEmailValue">
5661 <a href="{$url}">{$a['filename']}</a>
5665 $this->email->cid2Link($a['id'], $a['file_mime_type']);
5671 if($this->attachmentCount > 0) {
5672 $theCount = $this->attachmentCount;
5674 for($i=0; $i<$theCount; $i++) {
5675 $lbl = ($i == 0) ? $app_strings['LBL_EMAIL_ATTACHMENTS'].":" : '';
5676 $name = $this->getTempFilename(true).$i;
5677 $tempName = urlencode($this->tempAttachment[$name]);
5679 $url = "index.php?entryPoint=download&type=temp&isTempFile=true&ieId={$this->id}&tempName={$tempName}&id={$name}";
5681 $attachments .=<<<eoq
5683 <td NOWRAP valign="top" class="displayEmailLabel">
5686 <td NOWRAP valign="top" colspan="2" class="displayEmailValue">
5687 <a href="{$url}">{$this->tempAttachment[$name]}</a>
5694 $meta['email']['attachments'] = $attachments;
5697 $meta['email']['toaddrs'] = $this->collapseLongMailingList($this->email->to_addrs);
5698 $meta['email']['cc_addrs'] = $ccs;
5701 $description = (empty($this->email->description_html)) ? nl2br($this->email->description) : $this->email->description_html;
5702 $meta['email']['description'] = $description;
5705 $meta['is_sugarEmail'] = ($exMbox[0] == 'sugar') ? true : false;
5707 if(!$meta['is_sugarEmail']) {
5708 if($this->isAutoImport) {
5709 $meta['is_sugarEmail'] = true;
5712 if( $this->email->status != 'sent' ){
5713 // mark SugarEmail read
5714 $q = "UPDATE emails SET status = 'read' WHERE id = '{$uid}'";
5715 $r = $this->db->query($q);
5720 $meta['email']['name'] = to_html($this->email->name);
5721 $meta['email']['from_addr'] = ( !empty($this->email->from_addr_name) ) ? to_html($this->email->from_addr_name) : to_html($this->email->from_addr);
5722 $meta['email']['toaddrs'] = ( !empty($this->email->to_addrs_names) ) ? to_html($this->email->to_addrs_names) : to_html($this->email->to_addrs);
5723 $meta['email']['cc_addrs'] = to_html($this->email->cc_addrs_names);
5724 $meta['email']['reply_to_addr'] = to_html($this->email->reply_to_addr);
5725 $return['meta'] = $meta;
5731 * Takes a long list of email addresses from a To or CC field and shows the first 3, the rest hidden
5732 * @param string emails
5735 function collapseLongMailingList($emails) {
5736 global $app_strings;
5738 $ex = explode(",", $emails);
5742 if(count($ex) > 3) {
5746 foreach($ex as $email) {
5748 if(!empty($emails)) {
5751 $emails .= trim($email);
5753 if(!empty($emailsHidden)) {
5754 $emailsHidden .= ", ";
5756 $emailsHidden .= trim($email);
5762 if(!empty($emailsHidden)) {
5764 $emails = "<span onclick='javascript:SUGAR.email2.detailView.showFullEmailList(this);' style='cursor:pointer;'>{$emails} [...{$j} {$app_strings['LBL_MORE']}]</span>";
5765 $emailsHidden = "<span onclick='javascript:SUGAR.email2.detailView.showCroppedEmailList(this)' style='cursor:pointer; display:none;'>{$email2}, {$emailsHidden} [ {$app_strings['LBL_LESS']} ]</span>";
5768 $emails .= $emailsHidden;
5776 * Sorts IMAP's imap_fetch_overview() results
5777 * @param array $arr Array of standard objects
5778 * @param string $sort Column to sort by
5779 * @param string direction Direction to sort by (asc/desc)
5780 * @return array Sorted array of obj.
5782 function sortFetchedOverview($arr, $sort=4, $direction='DESC', $forceSeen=false) {
5783 global $current_user;
5785 $sortPrefs = $current_user->getPreference('folderSortOrder', 'Emails');
5786 if(!empty($sortPrefs))
5787 $listPrefs = $sortPrefs;
5789 $listPrefs = array();
5791 if(isset($listPrefs[$this->id][$this->mailbox])) {
5792 $currentNode = $listPrefs[$this->id][$this->mailbox];
5795 if(isset($currentNode['current']) && !empty($currentNode['current'])) {
5796 $sort = $currentNode['current']['sort'];
5797 $direction = $currentNode['current']['direction'];
5802 $sort = $this->defaultSort;//4;
5803 $direction = $this->defaultDirection; //'DESC';
5804 } elseif(!is_numeric($sort)) {
5805 // handle bad sort index
5806 $sort = $this->defaultSort;
5808 // translate numeric index to human readable
5809 $sort = $this->hrSort[$sort];
5811 if(empty($direction)) {
5812 $direction = 'DESC';
5820 foreach($arr as $k => $overview) {
5821 $sorts['flagged'][$k] = $overview->flagged;
5822 $sorts['status'][$k] = $overview->answered;
5823 $sorts['from'][$k] = str_replace('"', "", $this->handleMimeHeaderDecode($overview->from));
5824 $sorts['subj'][$k] = $this->handleMimeHeaderDecode(quoted_printable_decode($overview->subject));
5825 $sorts['date'][$k] = $overview->date;
5829 natcasesort($sorts[$sort]);
5830 //_ppd($sorts[$sort]);
5832 if(strtolower($direction) == 'desc') {
5833 $revSorts = array();
5834 $keys = array_reverse(array_keys($sorts[$sort]));
5835 // _pp("count keys in DESC: ".count($keys));
5836 // _pp("count elements in sort[sort]: ".count($sorts[$sort]));
5838 for($i=0; $i<count($keys); $i++) {
5840 $revSorts[$v] = $sorts[$sort][$v];
5843 //_pp("count post-sort: ".count($revSorts));
5844 $sorts[$sort] = $revSorts;
5846 $timedate = TimeDate::getInstance();
5847 foreach($sorts[$sort] as $k2 => $overview2) {
5848 $arr[$k2]->date = $timedate->fromString($arr[$k2]->date)->asDb();
5849 $retArr[] = $arr[$k2];
5851 //_pp("final count: ".count($retArr));
5853 $finalReturn = array();
5854 $finalReturn['retArr'] = $retArr;
5855 $finalReturn['sortBy'] = $sort;
5856 $finalReturn['direction'] = $direction;
5857 return $finalReturn;
5861 function setReadFlagOnFolderCache($mbox, $uid) {
5862 global $sugar_config;
5864 $this->mailbox = $mbox;
5867 if($this->validCacheExists($this->mailbox)) {
5868 $ret = $this->getCacheValue($this->mailbox);
5872 foreach($ret as $k => $v) {
5873 if($v->imap_uid == $uid) {
5880 $this->setCacheValue($this->mailbox, array(), $updates);
5885 * Returns a list of emails in a mailbox.
5886 * @param string mbox Name of mailbox using dot notation paths to display
5887 * @param string $forceRefresh Flag to use cache or not
5889 function displayFolderContents($mbox, $forceRefresh='false', $page) {
5890 global $current_user;
5892 $delimiter = $this->get_stored_options('folderDelimiter');
5894 $mbox = str_replace('.', $delimiter, $mbox);
5897 $this->mailbox = $mbox;
5899 // jchi #9424, get sort and direction from user preference
5901 $direction = 'desc';
5902 $sortSerial = $current_user->getPreference('folderSortOrder', 'Emails');
5903 if(!empty($sortSerial) && !empty($_REQUEST['ieId']) && !empty($_REQUEST['mbox'])) {
5904 $sortArray = unserialize($sortSerial);
5905 $sort = $sortArray[$_REQUEST['ieId']][$_REQUEST['mbox']]['current']['sort'];
5906 $direction = $sortArray[$_REQUEST['ieId']][$_REQUEST['mbox']]['current']['direction'];
5911 if(!empty($_REQUEST['sort']) && !empty($_REQUEST['dir'])) {
5912 $this->email->et->saveListViewSortOrder($_REQUEST['ieId'], $_REQUEST['mbox'], $_REQUEST['sort'], $_REQUEST['dir']);
5913 $sort = $_REQUEST['sort'];
5914 $direction = $_REQUEST['dir'];
5916 $_REQUEST['sort'] = '';
5917 $_REQUEST['dir'] = '';
5923 if($forceRefresh == 'false' && $this->validCacheExists($this->mailbox)) {
5924 $emailSettings = $current_user->getPreference('emailSettings', 'Emails');
5926 // cn: default to a low number until user specifies otherwise
5927 if(empty($emailSettings['showNumInList'])) {
5928 $emailSettings['showNumInList'] = 20;
5931 $ret = $this->getCacheValue($this->mailbox, $emailSettings['showNumInList'], $page, $sort, $direction);
5935 $out = $this->displayFetchedSortedListXML($ret, $mbox);
5937 $metadata = array();
5938 $metadata['mbox'] = $mbox;
5939 $metadata['ieId'] = $this->id;
5940 $metadata['name'] = $this->name;
5941 $metadata['fromCache'] = $cacheUsed ? 1 : 0;
5942 $metadata['out'] = $out;
5948 * For a group email account, create subscriptions for all users associated with the
5949 * team assigned to the account.
5952 function createUserSubscriptionsForGroupAccount()
5955 $team->retrieve($this->team_id);
5956 $usersList = $team->get_team_members(true);
5957 foreach($usersList as $userObject)
5959 $previousSubscriptions = unserialize(base64_decode($userObject->getPreference('showFolders', 'Emails',$userObject)));
5960 if($previousSubscriptions === FALSE)
5961 $previousSubscriptions = array();
5963 $previousSubscriptions[] = $this->id;
5965 $encodedSubs = base64_encode(serialize($previousSubscriptions));
5966 $userObject->setPreference('showFolders',$encodedSubs , '', 'Emails');
5967 $userObject->savePreferencesToDB();
5971 * Create a sugar folder for this inbound email account
5972 * if the Enable Auto Import option is selected
5974 * @return String Id of the sugar folder created.
5976 function createAutoImportSugarFolder()
5978 global $current_user;
5979 $guid = create_guid();
5980 $GLOBALS['log']->debug("Creating Sugar Folder for IE with id $guid");
5981 $folder = new SugarFolder();
5982 $folder->id = $guid;
5983 $folder->new_with_id = TRUE;
5984 $folder->name = $this->name;
5985 $folder->has_child = 0;
5986 $folder->is_group = 1;
5987 $folder->assign_to_id = $current_user->id;
5988 $folder->parent_folder = "";
5991 //If this inbound email is marked as inactive, don't add subscriptions.
5992 $addSubscriptions = ($this->status == 'Inactive' || $this->mailbox_type == 'bounce') ? FALSE : TRUE;
5993 $folder->save($addSubscriptions);
5998 function validCacheExists($mbox) {
5999 $q = "SELECT count(*) c FROM email_cache WHERE ie_id = '{$this->id}'";
6000 $r = $this->db->query($q);
6001 $a = $this->db->fetchByAssoc($r);
6014 function displayFetchedSortedListXML($ret, $mbox) {
6017 global $current_user;
6018 global $sugar_config;
6020 if(empty($ret['retArr'])) {
6024 $tPref = $current_user->getUserDateTimePreferences();
6028 foreach($ret['retArr'] as $msg) {
6030 $flagged = ($msg->flagged == 0) ? "" : $this->iconFlagged;
6031 $status = ($msg->deleted) ? $this->iconDeleted : "";
6032 $status = ($msg->draft == 0) ? $status : $this->iconDraft;
6033 $status = ($msg->answered == 0) ? $status : $this->iconAnswered;
6034 $from = $this->handleMimeHeaderDecode($msg->from);
6035 $subject = $this->handleMimeHeaderDecode($msg->subject);
6036 //$date = date($tPref['date']." ".$tPref['time'], $msg->date);
6037 $date = $timedate->to_display_date_time($this->db->fromConvert($msg->date, 'datetime'));
6038 //$date = date($tPref['date'], $this->getUnixHeaderDate($msg->date));
6041 $temp['flagged'] = $flagged;
6042 $temp['status'] = $status;
6043 $temp['from'] = to_html($from);
6044 $temp['subject'] = $subject;
6045 $temp['date'] = $date;
6046 $temp['uid'] = $msg->uid; // either from an imap_search() or massaged cache value
6047 $temp['mbox'] = $this->mailbox;
6048 $temp['ieId'] = $this->id;
6049 $temp['site_url'] = $sugar_config['site_url'];
6050 $temp['seen'] = $msg->seen;
6051 $temp['type'] = (isset($msg->type)) ? $msg->type: 'remote';
6052 $temp['to_addrs'] = to_html($msg->to);
6053 $temp['hasAttach'] = '0';
6064 * retrieves the mailboxes for a given account in the following format
6069 [Builder] => Builder
6076 * @param bool $justRaw Default false
6079 function getMailboxes($justRaw=false) {
6080 if($justRaw == true) {
6081 return $this->mailboxarray;
6084 return $this->generateMultiDimArrayFromFlatArray($this->mailboxarray, $this->retrieveDelimiter());
6086 $serviceString = $this->getConnectString('', '', false);
6088 if(strpos($serviceString, 'pop3')) {
6090 $obj->name = $serviceString."INBOX";
6091 $boxes = array("INBOX" => $obj);
6093 $boxes = imap_getmailboxes($this->conn, $serviceString, "*");
6098 // clean MBOX path names
6099 foreach($boxes as $k => $mbox) {
6100 $raw[] = str_replace($serviceString, "", $mbox->name);
6101 if ($mbox->delimiter) {
6102 $delimiter = $mbox->delimiter;
6105 $storedOptions = unserialize(base64_decode($this->stored_options));
6106 $storedOptions['folderDelimiter'] = $delimiter;
6107 $this->stored_options = base64_encode(serialize($storedOptions));
6113 // used by $this->search()
6114 if($justRaw == true) {
6119 // generate a multi-dimensional array to iterate through
6121 foreach($raw as $mbox) {
6122 $ret = $this->sortMailboxes($mbox, $ret, $delimiter);
6129 function getMailBoxesForGroupAccount() {
6130 $mailboxes = $this->generateMultiDimArrayFromFlatArray(explode(",", $this->mailbox), $this->retrieveDelimiter());
6131 $mailboxesArray = $this->generateFlatArrayFromMultiDimArray($mailboxes, $this->retrieveDelimiter());
6132 $mailboxesArray = $this->filterMailBoxFromRaw(explode(",", $this->mailbox), $mailboxesArray);
6133 $this->saveMailBoxFolders($mailboxesArray);
6135 if ($this->mailbox != $this->$email_user) {
6136 $mailboxes = $this->sortMailboxes($this->mailbox, $this->retrieveDelimiter());
6137 $mailboxesArray = $this->generateFlatArrayFromMultiDimArray($mailboxes, $this->retrieveDelimiter());
6138 $this->saveMailBoxFolders($mailboxesArray);
6139 // save mailbox value of an inbound email account to email user
6140 $this->saveMailBoxValueOfInboundEmail();
6142 $mailboxes = $this->getMailboxes();
6148 function saveMailBoxFolders($value) {
6149 if (is_array($value)) {
6150 $value = implode(",", $value);
6152 $this->mailboxarray = explode(",", $value);
6153 $value = $this->db->quoted($value);
6154 $query = "update inbound_email set mailbox = $value where id ='{$this->id}'";
6155 $this->db->query($query);
6158 function insertMailBoxFolders($value) {
6159 $query = "select value from config where category='InboundEmail' and name='{$this->id}'";
6160 $r = $this->db->query($query);
6161 $a = $this->db->fetchByAssoc($r);
6162 if (empty($a['value'])) {
6163 if (is_array($value)) {
6164 $value = implode(",", $value);
6166 $this->mailboxarray = explode(",", $value);
6167 $value = $this->db->quoted($value);
6169 $query = "INSERT INTO config VALUES('InboundEmail', '{$this->id}', $value)";
6170 $this->db->query($query);
6174 function saveMailBoxValueOfInboundEmail() {
6175 $query = "update Inbound_email set mailbox = '{$this->email_user}'";
6176 $this->db->query($query);
6179 function retrieveMailBoxFolders() {
6180 $this->mailboxarray = explode(",", $this->mailbox);
6182 $query = "select value from config where category='InboundEmail' and name='{$this->id}'";
6183 $r = $this->db->query($query);
6184 $a = $this->db->fetchByAssoc($r);
6185 $this->mailboxarray = explode(",", $a['value']);
6190 function retrieveDelimiter() {
6191 $delimiter = $this->get_stored_options('folderDelimiter');
6198 function generateFlatArrayFromMultiDimArray($arraymbox, $delimiter) {
6200 foreach($arraymbox as $key => $value) {
6201 $this->generateArrayData($key, $value, $ret, $delimiter);
6207 function generateMultiDimArrayFromFlatArray($raw, $delimiter) {
6208 // generate a multi-dimensional array to iterate through
6210 foreach($raw as $mbox) {
6211 $ret = $this->sortMailboxes($mbox, $ret, $delimiter);
6217 function generateArrayData($key, $arraymbox, &$ret, $delimiter) {
6219 if (is_array($arraymbox)) {
6220 foreach($arraymbox as $mboxKey => $value) {
6221 $newKey = $key . $delimiter . $mboxKey;
6222 $this->generateArrayData($newKey, $value, $ret, $delimiter);
6228 * sorts the folders in a mailbox in a multi-dimensional array
6229 * @param string $MBOX
6233 function sortMailboxes($mbox, $ret, $delimeter = ".") {
6234 if(strpos($mbox, $delimeter)) {
6235 $node = substr($mbox, 0, strpos($mbox, $delimeter));
6236 $nodeAfter = substr($mbox, strpos($mbox, $node) + strlen($node) + 1, strlen($mbox));
6238 if(!isset($ret[$node])) {
6239 $ret[$node] = array();
6240 } elseif(isset($ret[$node]) && !is_array($ret[$node])) {
6241 $ret[$node] = array();
6243 $ret[$node] = $this->sortMailboxes($nodeAfter, $ret[$node], $delimeter);
6245 $ret[$mbox] = $mbox;
6252 * parses Sugar's storage method for imap server service strings
6255 function getServiceString() {
6257 $exServ = explode('::', $this->service);
6259 foreach($exServ as $v) {
6260 if(!empty($v) && ($v != 'imap' && $v !='pop3')) {
6269 * Get Email messages IDs from server which aren't in database
6270 * @return array Ids of messages, which aren't still in database
6272 public function getNewEmailsForSyncedMailbox()
6274 // ids's count limit for batch processing
6276 $msgIds = imap_search($this->conn, 'ALL UNDELETED');
6279 if(count($msgIds) > 0)
6282 * @var collect results of queries and message headers
6288 // sort IDs to get lastest on top
6290 $GLOBALS['log']->debug('-----> getNewEmailsForSyncedMailbox() got '.count($msgIds).' Messages');
6291 foreach($msgIds as $k => &$msgNo)
6293 $uid = imap_uid($this->conn, $msgNo);
6294 $header = imap_headerinfo($this->conn, $msgNo);
6295 $fullHeader = imap_fetchheader($this->conn, $msgNo);
6296 $message_id = $header->message_id;
6297 $deliveredTo = $this->id;
6299 preg_match('/(delivered-to:|x-real-to:){1}\s*(\S+)\s*\n{1}/im', $fullHeader, $matches);
6302 $deliveredTo = $matches[2];
6304 if(empty($message_id) || !isset($message_id))
6306 $GLOBALS['log']->debug('*********** NO MESSAGE_ID.');
6307 $message_id = $this->getMessageId($header);
6310 // generate compound messageId
6311 $this->compoundMessageId = trim($message_id) . trim($deliveredTo);
6312 // if the length > 255 then md5 it so that the data will be of smaller length
6313 if (strlen($this->compoundMessageId) > 255)
6315 $this->compoundMessageId = md5($this->compoundMessageId);
6318 if (empty($this->compoundMessageId))
6323 $potentials = clean_xss($this->compoundMessageId, false);
6325 if(is_array($potentials) && !empty($potentials))
6327 foreach($potentials as $bad)
6329 $this->compoundMessageId = str_replace($bad, "", $this->compoundMessageId);
6332 array_push($tmpMsgs, array('msgNo' => $msgNo, 'msgId' => $this->compoundMessageId, 'exists' => 0));
6333 if($counter == $limit)
6337 foreach(array_slice($tmpMsgs, -$limit, $limit) as $k1 => $v1)
6339 $query[] = $v1['msgId'];
6341 $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';
6342 $r = $this->db->query($query);
6344 while($a = $this->db->fetchByAssoc($r))
6346 $tmp[html_entity_decode($a['mid'])] = $a['cnt'];
6348 foreach($tmpMsgs as $k1 => $v1)
6350 if(isset($tmp[$v1['msgId']]) && $tmp[$v1['msgId']] > 0)
6352 $tmpMsgs[$k1]['exists'] = 1;
6355 foreach($tmpMsgs as $k1 => $v1)
6357 if($v1['exists'] == 0)
6360 array_push($result, $v1['msgNo']);
6367 if($repeats >= $limit)
6373 $tmpMsgs = array_splice($tmpMsgs, -$repeats, $repeats);
6384 }catch(Exception $ex)
6386 $GLOBALS['log']->fatal($ex->getMessage());
6388 $GLOBALS['log']->debug('-----> getNewEmailsForSyncedMailbox() got '.count($result).' unsynced messages');
6393 * Import new messages from given account.
6395 public function importMessages()
6397 $protocol = $this->isPop3Protocol() ? 'pop3' : 'imap';
6398 switch ($protocol) {
6400 $this->importMailboxMessages($protocol);
6403 $mailboxes = $this->getMailboxes(true);
6404 foreach ($mailboxes as $mailbox) {
6405 $this->importMailboxMessages($protocol, $mailbox);
6407 imap_expunge($this->conn);
6408 imap_close($this->conn);
6414 * Import messages from specified mailbox
6416 * @param string $protocol Mailing protocol
6417 * @param string|null $mailbox Mailbox (if applied to protocol)
6419 protected function importMailboxMessages($protocol, $mailbox = null)
6421 switch ($protocol) {
6423 $msgNumbers = $this->getPop3NewMessagesToDownload();
6426 $this->mailbox = $mailbox;
6427 $this->connectMailserver();
6428 $msgNumbers = $this->getNewMessageIds();
6430 $msgNumbers = array();
6434 $msgNumbers = array();
6438 foreach ($msgNumbers as $msgNumber) {
6439 $uid = $this->getMessageUID($msgNumber, $protocol);
6440 $GLOBALS['log']->info('Importing message no: ' . $msgNumber);
6441 $this->importOneEmail($msgNumber, $uid, false, false);
6446 * Retrieves message UID by it's number
6448 * @param int $msgNumber Number of the message in current sequence
6449 * @param string $protocol Mailing protocol
6452 protected function getMessageUID($msgNumber, $protocol)
6454 switch ($protocol) {
6456 $uid = $this->getUIDLForMessage($msgNumber);
6459 $uid = imap_uid($this->conn, $msgNumber);
6468 } // end class definition
6472 * Simple class to mirror the passed object from an imap_fetch_overview() call
6491 var $indices; /* = array(
6494 'name' => 'mail_date',
6502 'name' => 'mail_from',
6510 'name' => 'mail_subj',
6519 var $fieldDefs;/* = array(
6522 'type' => 'varchar',
6527 'name' => 'subject',
6528 'type' => 'varchar',
6530 'required' => false,
6532 'fromaddr' => array(
6533 'name' => 'fromaddr',
6534 'type' => 'varchar',
6540 'type' => 'varchar',
6544 'senddate' => array(
6545 'name' => 'senddate',
6546 'type' => 'datetime',
6549 'message_id' => array(
6550 'name' => 'message_id',
6551 'type' => 'varchar',
6553 'required' => false,
6555 'mailsize' => array(
6556 'name' => 'mailsize',
6571 'required' => false,
6575 'type' => 'tinyint',
6580 'name' => 'flagged',
6581 'type' => 'tinyint',
6585 'answered' => array(
6586 'name' => 'answered',
6587 'type' => 'tinyint',
6592 'name' => 'deleted',
6593 'type' => 'tinyint',
6599 'type' => 'tinyint',
6605 'type' => 'tinyint',
6611 function Overview() {
6614 if(!isset($dictionary['email_cache']) || empty($dictionary['email_cache'])) {
6615 if(file_exists('custom/metadata/email_cacheMetaData.php')) {
6616 include('custom/metadata/email_cacheMetaData.php');
6618 include('metadata/email_cacheMetaData.php');
6622 $this->fieldDefs = $dictionary['email_cache']['fields'];
6623 $this->indices = $dictionary['email_cache']['indices'];