[ Index ] |
PHP Cross Reference of Eventum |
[Summary view] [Print] [Text view]
1 <?php 2 /* vim: set expandtab tabstop=4 shiftwidth=4 encoding=utf-8: */ 3 // +----------------------------------------------------------------------+ 4 // | Eventum - Issue Tracking System | 5 // +----------------------------------------------------------------------+ 6 // | Copyright (c) 2003, 2004, 2005, 2006, 2007 MySQL AB | 7 // | | 8 // | This program is free software; you can redistribute it and/or modify | 9 // | it under the terms of the GNU General Public License as published by | 10 // | the Free Software Foundation; either version 2 of the License, or | 11 // | (at your option) any later version. | 12 // | | 13 // | This program is distributed in the hope that it will be useful, | 14 // | but WITHOUT ANY WARRANTY; without even the implied warranty of | 15 // | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 16 // | GNU General Public License for more details. | 17 // | | 18 // | You should have received a copy of the GNU General Public License | 19 // | along with this program; if not, write to: | 20 // | | 21 // | Free Software Foundation, Inc. | 22 // | 59 Temple Place - Suite 330 | 23 // | Boston, MA 02111-1307, USA. | 24 // +----------------------------------------------------------------------+ 25 // | Authors: João Prado Maia <jpm@mysql.com> | 26 // +----------------------------------------------------------------------+ 27 // 28 29 require_once (APP_INC_PATH . "class.error_handler.php"); 30 require_once (APP_INC_PATH . "class.auth.php"); 31 require_once (APP_INC_PATH . "class.user.php"); 32 require_once (APP_INC_PATH . "class.pager.php"); 33 require_once (APP_INC_PATH . "class.mail.php"); 34 require_once (APP_INC_PATH . "class.note.php"); 35 require_once (APP_INC_PATH . "class.misc.php"); 36 require_once (APP_INC_PATH . "class.mime_helper.php"); 37 require_once (APP_INC_PATH . "class.date.php"); 38 require_once (APP_INC_PATH . "class.history.php"); 39 require_once (APP_INC_PATH . "class.issue.php"); 40 require_once (APP_INC_PATH . "class.email_account.php"); 41 require_once (APP_INC_PATH . "class.search_profile.php"); 42 require_once (APP_INC_PATH . "class.routing.php"); 43 44 /** 45 * Class to handle the business logic related to the email feature of 46 * the application. 47 * 48 * @version 1.0 49 * @author João Prado Maia <jpm@mysql.com> 50 */ 51 52 class Support 53 { 54 /** 55 * Permanently removes the given support emails from the associated email 56 * server. 57 * 58 * @access public 59 * @param array $sup_ids The list of support emails 60 * @return integer 1 if the removal worked, -1 otherwise 61 */ 62 function expungeEmails($sup_ids) 63 { 64 $accounts = array(); 65 66 $stmt = "SELECT 67 sup_id, 68 sup_message_id, 69 sup_ema_id 70 FROM 71 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email 72 WHERE 73 sup_id IN (" . implode(', ', Misc::escapeInteger($sup_ids)) . ")"; 74 $res = $GLOBALS["db_api"]->dbh->getAll($stmt, DB_FETCHMODE_ASSOC); 75 if (PEAR::isError($res)) { 76 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 77 return -1; 78 } else { 79 for ($i = 0; $i < count($res); $i++) { 80 // don't remove emails from the imap/pop3 server if the email 81 // account is set to leave a copy of the messages on the server 82 $account_details = Email_Account::getDetails($res[$i]['sup_ema_id']); 83 if (!$account_details['leave_copy']) { 84 // try to re-use an open connection to the imap server 85 if (!in_array($res[$i]['sup_ema_id'], array_keys($accounts))) { 86 $accounts[$res[$i]['sup_ema_id']] = Support::connectEmailServer(Email_Account::getDetails($res[$i]['sup_ema_id'])); 87 } 88 $mbox = $accounts[$res[$i]['sup_ema_id']]; 89 if ($mbox !== FALSE) { 90 // now try to find the UID of the current message-id 91 $matches = @imap_search($mbox, 'TEXT "' . $res[$i]['sup_message_id'] . '"'); 92 if (count($matches) > 0) { 93 for ($y = 0; $y < count($matches); $y++) { 94 $headers = imap_headerinfo($mbox, $matches[$y]); 95 // if the current message also matches the message-id header, then remove it! 96 if ($headers->message_id == $res[$i]['sup_message_id']) { 97 @imap_delete($mbox, $matches[$y]); 98 @imap_expunge($mbox); 99 break; 100 } 101 } 102 } 103 } 104 } 105 // remove the email record from the table 106 Support::removeEmail($res[$i]['sup_id']); 107 } 108 return 1; 109 } 110 } 111 112 113 /** 114 * Removes the given support email from the database table. 115 * 116 * @access public 117 * @param integer $sup_id The support email ID 118 * @return boolean 119 */ 120 function removeEmail($sup_id) 121 { 122 $stmt = "DELETE FROM 123 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email 124 WHERE 125 sup_id=" . Misc::escapeInteger($sup_id); 126 $res = $GLOBALS["db_api"]->dbh->query($stmt); 127 if (PEAR::isError($res)) { 128 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 129 return false; 130 } else { 131 $stmt = "DELETE FROM 132 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email_body 133 WHERE 134 seb_sup_id=" . Misc::escapeInteger($sup_id); 135 $res = $GLOBALS["db_api"]->dbh->query($stmt); 136 if (PEAR::isError($res)) { 137 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 138 return false; 139 } else { 140 return true; 141 } 142 } 143 } 144 145 146 /** 147 * Method used to get the next and previous messages in order to build 148 * side links when viewing a particular email. 149 * 150 * @access public 151 * @param integer $sup_id The email ID 152 * @return array Information on the next and previous messages 153 */ 154 function getListingSides($sup_id) 155 { 156 $options = Support::saveSearchParams(); 157 158 $stmt = "SELECT 159 sup_id, 160 sup_ema_id 161 FROM 162 ( 163 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email, 164 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "email_account 165 ) 166 LEFT JOIN 167 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 168 ON 169 sup_iss_id = iss_id"; 170 if (!empty($options['keywords'])) { 171 $stmt .= "," . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email_body"; 172 } 173 $stmt .= Support::buildWhereClause($options); 174 $stmt .= " 175 ORDER BY 176 " . $options["sort_by"] . " " . $options["sort_order"]; 177 $res = $GLOBALS["db_api"]->dbh->getAssoc($stmt); 178 if (PEAR::isError($res)) { 179 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 180 return ""; 181 } else { 182 // COMPAT: the next line requires PHP >= 4.0.5 183 $email_ids = array_keys($res); 184 $index = array_search($sup_id, $email_ids); 185 if (!empty($email_ids[$index+1])) { 186 $next = $email_ids[$index+1]; 187 } 188 if (!empty($email_ids[$index-1])) { 189 $previous = $email_ids[$index-1]; 190 } 191 return array( 192 "next" => array( 193 'sup_id' => @$next, 194 'ema_id' => @$res[$next] 195 ), 196 "previous" => array( 197 'sup_id' => @$previous, 198 'ema_id' => @$res[$previous] 199 ) 200 ); 201 } 202 } 203 204 205 /** 206 * Method used to get the next and previous messages in order to build 207 * side links when viewing a particular email associated with an issue. 208 * 209 * @access public 210 * @param integer $issue_id The issue ID 211 * @param integer $sup_id The email ID 212 * @return array Information on the next and previous messages 213 */ 214 function getIssueSides($issue_id, $sup_id) 215 { 216 $stmt = "SELECT 217 sup_id, 218 sup_ema_id 219 FROM 220 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email 221 WHERE 222 sup_iss_id=" . Misc::escapeInteger($issue_id) . " 223 ORDER BY 224 sup_id ASC"; 225 $res = $GLOBALS["db_api"]->dbh->getAssoc($stmt); 226 if (PEAR::isError($res)) { 227 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 228 return ""; 229 } else { 230 // COMPAT: the next line requires PHP >= 4.0.5 231 $email_ids = array_keys($res); 232 $index = array_search($sup_id, $email_ids); 233 if (!empty($email_ids[$index+1])) { 234 $next = $email_ids[$index+1]; 235 } 236 if (!empty($email_ids[$index-1])) { 237 $previous = $email_ids[$index-1]; 238 } 239 return array( 240 "next" => array( 241 'sup_id' => @$next, 242 'ema_id' => @$res[$next] 243 ), 244 "previous" => array( 245 'sup_id' => @$previous, 246 'ema_id' => @$res[$previous] 247 ) 248 ); 249 } 250 } 251 252 253 /** 254 * Method used to save the email note into a backup directory. 255 * 256 * @access public 257 * @param string $message The full body of the email 258 */ 259 function saveRoutedEmail($message) 260 { 261 list($usec,) = explode(" ", microtime()); 262 $filename = date('Y-m-d_H-i-s_') . $usec . '.email.txt'; 263 $file = APP_ROUTED_MAILS_SAVEDIR . 'routed_emails/' . $filename; 264 $fp = @fopen($file, 'w'); 265 @fwrite($fp, $message); 266 @fclose($fp); 267 @chmod($file, 0644); 268 } 269 270 271 /** 272 * Method used to get the sender of a given set of emails. 273 * 274 * @access public 275 * @param integer $sup_ids The email IDs 276 * @return array The 'From:' headers for those emails 277 */ 278 function getSender($sup_ids) 279 { 280 $stmt = "SELECT 281 sup_from 282 FROM 283 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email 284 WHERE 285 sup_id IN (" . implode(", ", Misc::escapeInteger($sup_ids)) . ")"; 286 $res = $GLOBALS["db_api"]->dbh->getCol($stmt); 287 if (PEAR::isError($res)) { 288 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 289 return array(); 290 } else { 291 if (empty($res)) { 292 return array(); 293 } else { 294 return $res; 295 } 296 } 297 } 298 299 300 /** 301 * Method used to clear the error stack as required by the IMAP PHP extension. 302 * 303 * @access public 304 * @return void 305 */ 306 function clearErrors() 307 { 308 @imap_errors(); 309 } 310 311 312 /** 313 * Method used to restore the specified support emails from 314 * 'removed' to 'active'. 315 * 316 * @access public 317 * @return integer 1 if the update worked, -1 otherwise 318 */ 319 function restoreEmails() 320 { 321 $items = @implode(", ", Misc::escapeInteger($_POST["item"])); 322 $stmt = "UPDATE 323 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email 324 SET 325 sup_removed=0 326 WHERE 327 sup_id IN ($items)"; 328 $res = $GLOBALS["db_api"]->dbh->query($stmt); 329 if (PEAR::isError($res)) { 330 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 331 return -1; 332 } else { 333 return 1; 334 } 335 } 336 337 338 /** 339 * Method used to get the list of support email entries that are 340 * set as 'removed'. 341 * 342 * @access public 343 * @return array The list of support emails 344 */ 345 function getRemovedList() 346 { 347 $stmt = "SELECT 348 sup_id, 349 sup_date, 350 sup_subject, 351 sup_from 352 FROM 353 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email, 354 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "email_account 355 WHERE 356 ema_prj_id=" . Auth::getCurrentProject() . " AND 357 ema_id=sup_ema_id AND 358 sup_removed=1"; 359 $res = $GLOBALS["db_api"]->dbh->getAll($stmt, DB_FETCHMODE_ASSOC); 360 if (PEAR::isError($res)) { 361 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 362 return ""; 363 } else { 364 for ($i = 0; $i < count($res); $i++) { 365 $res[$i]["sup_date"] = Date_API::getFormattedDate($res[$i]["sup_date"]); 366 $res[$i]["sup_subject"] = Mime_Helper::fixEncoding($res[$i]["sup_subject"]); 367 $res[$i]["sup_from"] = Mime_Helper::fixEncoding($res[$i]["sup_from"]); 368 } 369 return $res; 370 } 371 } 372 373 374 /** 375 * Method used to remove all support email entries associated with 376 * a specified list of support email accounts. 377 * 378 * @access public 379 * @param array $ids The list of support email accounts 380 * @return boolean 381 */ 382 function removeEmailByAccounts($ids) 383 { 384 if (count($ids) < 1) { 385 return true; 386 } 387 $items = @implode(", ", Misc::escapeInteger($ids)); 388 $stmt = "DELETE FROM 389 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email 390 WHERE 391 sup_ema_id IN ($items)"; 392 $res = $GLOBALS["db_api"]->dbh->query($stmt); 393 if (PEAR::isError($res)) { 394 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 395 return false; 396 } else { 397 return true; 398 } 399 } 400 401 402 /** 403 * Method used to build the server URI to connect to. 404 * 405 * @access public 406 * @param array $info The email server information 407 * @param boolean $tls Whether to use TLS or not 408 * @return string The server URI to connect to 409 */ 410 function getServerURI($info, $tls = FALSE) 411 { 412 $server_uri = $info['ema_hostname'] . ':' . $info['ema_port'] . '/' . strtolower($info['ema_type']); 413 if (stristr($info['ema_type'], 'imap')) { 414 $folder = $info['ema_folder']; 415 } else { 416 $folder = 'INBOX'; 417 } 418 return '{' . $server_uri . '}' . $folder; 419 } 420 421 422 /** 423 * Method used to connect to the provided email server. 424 * 425 * @access public 426 * @param array $info The email server information 427 * @return resource The email server connection 428 */ 429 function connectEmailServer($info) 430 { 431 $mbox = @imap_open(Support::getServerURI($info), $info['ema_username'], $info['ema_password']); 432 if ($mbox === FALSE) { 433 $errors = @imap_errors(); 434 if (strstr(strtolower($errors[0]), 'certificate failure')) { 435 $mbox = @imap_open(Support::getServerURI($info, TRUE), $info['ema_username'], $info['ema_password']); 436 } else { 437 Error_Handler::logError('Error while connecting to the email server - ' . $errors[0], __FILE__, __LINE__); 438 } 439 } 440 return $mbox; 441 } 442 443 444 /** 445 * Method used to get the total number of emails in the specified 446 * mailbox. 447 * 448 * @access public 449 * @param resource $mbox The mailbox 450 * @return integer The number of emails 451 */ 452 function getTotalEmails($mbox) 453 { 454 return @imap_num_msg($mbox); 455 } 456 457 458 /** 459 * Method used to get the information about a specific message 460 * from a given mailbox. 461 * 462 * XXX this function does more than that. 463 * 464 * @access public 465 * @param resource $mbox The mailbox 466 * @param array $info The support email account information 467 * @param integer $num The index of the message 468 * @return void 469 */ 470 function getEmailInfo($mbox, $info, $num) 471 { 472 Auth::createFakeCookie(APP_SYSTEM_USER_ID); 473 474 // check if the current message was already seen 475 if ($info['ema_get_only_new']) { 476 list($overview) = @imap_fetch_overview($mbox, $num); 477 if (($overview->seen) || ($overview->deleted) || ($overview->answered)) { 478 return; 479 } 480 } 481 482 $email = @imap_headerinfo($mbox, $num); 483 $headers = imap_fetchheader($mbox, $num); 484 $body = imap_body($mbox, $num); 485 // check for mysterious blank messages 486 if (empty($body) and empty($headers)) { 487 // XXX do some error reporting? 488 return; 489 } 490 $message_id = Mail_API::getMessageID($headers, $body); 491 $message = $headers . $body; 492 // we don't need $body anymore -- free memory 493 unset($body); 494 495 // if message_id already exists, return immediately -- nothing to do 496 if (Support::exists($message_id) || Note::exists($message_id)) { 497 return; 498 } 499 500 $structure = Mime_Helper::decode($message, true, true); 501 $message_body = Mime_Helper::getMessageBody($structure); 502 if (Mime_Helper::hasAttachments($structure)) { 503 $has_attachments = 1; 504 } else { 505 $has_attachments = 0; 506 } 507 // we can't trust the in-reply-to from the imap c-client, so let's 508 // try to manually parse that value from the full headers 509 $reference_msg_id = Mail_API::getReferenceMessageID($headers); 510 511 // pass in $email by reference so it can be modified 512 $workflow = Workflow::preEmailDownload($info['ema_prj_id'], $info, $mbox, $num, $message, $email); 513 if ($workflow === -1) { 514 return; 515 } 516 517 // route emails if neccassary 518 if ($info['ema_use_routing'] == 1) { 519 $setup = Setup::load(); 520 521 // we create addresses array so it can be reused 522 $addresses = array(); 523 if (isset($email->to)) { 524 foreach ($email->to as $address) { 525 $addresses[] = $address->mailbox . '@' . $address->host; 526 } 527 } 528 if (isset($email->cc)) { 529 foreach ($email->cc as $address) { 530 $addresses[] = $address->mailbox . '@' . $address->host; 531 } 532 } 533 534 if (@$setup['email_routing']['status'] == 'enabled') { 535 $res = Routing::getMatchingIssueIDs($addresses, 'email'); 536 if ($res != false) { 537 $return = Routing::route_emails($message); 538 if ($return == true) { 539 Support::deleteMessage($info, $mbox, $num); 540 return; 541 } 542 return; 543 } 544 } 545 if (@$setup['note_routing']['status'] == 'enabled') { 546 $res = Routing::getMatchingIssueIDs($addresses, 'note'); 547 if ($res != false) { 548 $return = Routing::route_notes($message); 549 if ($return == true) { 550 Support::deleteMessage($info, $mbox, $num); 551 return; 552 } 553 return; 554 } 555 } 556 if (@$setup['draft_routing']['status'] == 'enabled') { 557 $res = Routing::getMatchingIssueIDs($addresses, 'draft'); 558 if ($res != false) { 559 $return = Routing::route_drafts($message); 560 if ($return == true) { 561 Support::deleteMessage($info, $mbox, $num); 562 return; 563 } 564 return; 565 } 566 } 567 return; 568 } 569 570 $sender_email = Mail_API::getEmailAddress($email->fromaddress); 571 if (PEAR::isError($sender_email)) { 572 $sender_email = 'Error Parsing Email <>'; 573 } 574 575 $t = array( 576 'ema_id' => $info['ema_id'], 577 'message_id' => $message_id, 578 'date' => @Date_API::getDateGMTByTS($email->udate), 579 'from' => $sender_email, 580 'to' => @$email->toaddress, 581 'cc' => @$email->ccaddress, 582 'subject' => @$email->subject, 583 'body' => @$message_body, 584 'full_email' => @$message, 585 'has_attachment' => $has_attachments, 586 // the following items are not inserted, but useful in some methods 587 'headers' => @$structure->headers 588 ); 589 $should_create_array = Support::createIssueFromEmail( 590 $info, $headers, $message_body, $t['date'], $sender_email, Mime_Helper::fixEncoding( @$email->subject), $t['to'], $t['cc']); 591 $should_create_issue = $should_create_array['should_create_issue']; 592 $associate_email = $should_create_array['associate_email']; 593 if (!empty($should_create_array['issue_id'])) { 594 $t['issue_id'] = $should_create_array['issue_id']; 595 596 // figure out if we should change to a different email account 597 $iss_prj_id = Issue::getProjectID($t['issue_id']); 598 if ($info['ema_prj_id'] != $iss_prj_id) { 599 $new_ema_id = Email_Account::getEmailAccount($iss_prj_id); 600 if (!empty($new_ema_id)) { 601 $t['ema_id'] = $new_ema_id; 602 } 603 } 604 } 605 if (!empty($should_create_array['customer_id'])) { 606 $t['customer_id'] = $should_create_array['customer_id']; 607 } 608 if (empty($t['issue_id'])) { 609 $t['issue_id'] = 0; 610 } else { 611 $prj_id = Issue::getProjectID($t['issue_id']); 612 Auth::createFakeCookie(APP_SYSTEM_USER_ID, $prj_id); 613 } 614 if ($should_create_array['type'] == 'note') { 615 // assume that this is not a valid note 616 $res = -1; 617 618 if ($t['issue_id'] != 0) { 619 // check if this is valid user 620 $usr_id = User::getUserIDByEmail($sender_email); 621 if (!empty($usr_id)) { 622 $role_id = User::getRoleByUser($usr_id, $prj_id); 623 if ($role_id > User::getRoleID("Customer")) { 624 // actually a valid user so insert the note 625 626 Auth::createFakeCookie($usr_id, $prj_id); 627 628 $users = Project::getUserEmailAssocList($prj_id, 'active', User::getRoleID('Customer')); 629 $user_emails = array_map('strtolower', array_values($users)); 630 $users = array_flip($users); 631 632 $addresses = array(); 633 $to_addresses = Mail_API::getEmailAddresses(@$structure->headers['to']); 634 if (count($to_addresses)) { 635 $addresses = $to_addresses; 636 } 637 $cc_addresses = Mail_API::getEmailAddresses(@$structure->headers['cc']); 638 if (count($cc_addresses)) { 639 $addresses = array_merge($addresses, $cc_addresses); 640 } 641 $cc_users = array(); 642 foreach ($addresses as $email) { 643 if (in_array(strtolower($email), $user_emails)) { 644 $cc_users[] = $users[$email]; 645 } 646 } 647 648 // XXX FIXME, this is not nice thing to do 649 $_POST = array( 650 'title' => Mail_API::removeExcessRe($t['subject']), 651 'note' => $t['body'], 652 'note_cc' => $cc_users, 653 'add_extra_recipients' => 'yes', 654 'message_id' => $t['message_id'], 655 'parent_id' => $should_create_array['parent_id'], 656 ); 657 $res = Note::insert($usr_id, $t['issue_id']); 658 } 659 } 660 } 661 } else { 662 // check if we need to block this email 663 if (($should_create_issue == true) || (!Support::blockEmailIfNeeded($t))) { 664 if (!empty($t['issue_id'])) { 665 list($t['full_email'], $t['headers']) = Mail_API::rewriteThreadingHeaders($t['issue_id'], $t['full_email'], $t['headers'], 'email'); 666 } 667 668 // make variable available for workflow to be able to detect whether this email created new issue 669 $t['should_create_issue'] = $should_create_array['should_create_issue']; 670 671 $res = Support::insertEmail($t, $structure, $sup_id); 672 if ($res != -1) { 673 // only extract the attachments from the email if we are associating the email to an issue 674 if (!empty($t['issue_id'])) { 675 Support::extractAttachments($t['issue_id'], $structure); 676 677 // notifications about new emails are always external 678 $internal_only = false; 679 $assignee_only = false; 680 // special case when emails are bounced back, so we don't want a notification to customers about those 681 if (Notification::isBounceMessage($sender_email)) { 682 // broadcast this email only to the assignees for this issue 683 $internal_only = true; 684 $assignee_only = true; 685 } elseif ($should_create_issue == true) { 686 // if a new issue was created, only send a copy of the email to the assignee (if any), don't resend to the original TO/CC list 687 $assignee_only = true; 688 $internal_only = true; 689 } 690 Notification::notifyNewEmail(Auth::getUserID(), $t['issue_id'], $t, $internal_only, $assignee_only, '', $sup_id); 691 // try to get usr_id of sender, if not, use system account 692 $usr_id = User::getUserIDByEmail(Mail_API::getEmailAddress($structure->headers['from'])); 693 if (!$usr_id) { 694 $usr_id = APP_SYSTEM_USER_ID; 695 } 696 // mark this issue as updated 697 if ((!empty($t['customer_id'])) && ($t['customer_id'] != 'NULL')) { 698 Issue::markAsUpdated($t['issue_id'], 'customer action'); 699 } else { 700 if ((!empty($usr_id)) && (User::getRoleByUser($usr_id, $prj_id) > User::getRoleID('Customer'))) { 701 Issue::markAsUpdated($t['issue_id'], 'staff response'); 702 } else { 703 Issue::markAsUpdated($t['issue_id'], 'user response'); 704 } 705 } 706 // log routed email 707 History::add($t['issue_id'], $usr_id, History::getTypeID('email_routed'), ev_gettext('Email routed from %1$s', $structure->headers['from'])); 708 } 709 } 710 } else { 711 $res = 1; 712 } 713 } 714 715 if ($res > 0) { 716 // need to delete the message from the server? 717 if (!$info['ema_leave_copy']) { 718 @imap_delete($mbox, $num); 719 } else { 720 // mark the message as already read 721 @imap_setflag_full($mbox, $num, "\\Seen"); 722 } 723 } 724 return; 725 } 726 727 728 /** 729 * Creates a new issue from an email if appropriate. Also returns if this message is related 730 * to a previous message. 731 * 732 * @access private 733 * @param array $info An array of info about the email account. 734 * @param string $headers The headers of the email. 735 * @param string $message_body The body of the message. 736 * @param string $date The date this message was sent 737 * @param string $from The name and email address of the sender. 738 * @param string $subject The subject of this message. 739 * @param array $to An array of to addresses 740 * @param array $cc An array of cc addresses 741 * @return array An array of information about the message 742 */ 743 function createIssueFromEmail($info, $headers, $message_body, $date, $from, $subject, $to, $cc) 744 { 745 $should_create_issue = false; 746 $issue_id = ''; 747 $associate_email = ''; 748 $type = 'email'; 749 $parent_id = ''; 750 751 // we can't trust the in-reply-to from the imap c-client, so let's 752 // try to manually parse that value from the full headers 753 $references = Mail_API::getAllReferences($headers); 754 755 $message_id = Mail_API::getMessageID($headers, $message_body); 756 757 $setup = Setup::load(); 758 if (@$setup['subject_based_routing']['status'] == 'enabled') { 759 // Look for issue ID in the subject line 760 761 // look for [#XXXX] in the subject line 762 if (preg_match("/\[#(\d+)\]( Note| BLOCKED)*/", $subject, $matches)) { 763 $should_create_issue = false; 764 $issue_id = $matches[1]; 765 if (!Issue::exists($issue_id, false)) { 766 $issue_id = ''; 767 } elseif (!empty($matches[2])) { 768 $type = 'note'; 769 } 770 } else { 771 $should_create_issue = true; 772 } 773 } else { 774 // - if this email is a reply: 775 if (count($references) > 0) { 776 foreach ($references as $reference_msg_id) { 777 // -> check if the replied email exists in the database: 778 if (Note::exists($reference_msg_id)) { 779 // note exists 780 // get what issue it belongs too. 781 $issue_id = Note::getIssueByMessageID($reference_msg_id); 782 $should_create_issue = false; 783 $type = 'note'; 784 $parent_id = Note::getIDByMessageID($reference_msg_id); 785 break; 786 } elseif ((Support::exists($reference_msg_id)) || (Issue::getIssueByRootMessageID($reference_msg_id) != false)) { 787 // email or issue exists 788 $issue_id = Support::getIssueByMessageID($reference_msg_id); 789 if (empty($issue_id)) { 790 $issue_id = Issue::getIssueByRootMessageID($reference_msg_id); 791 } 792 if (empty($issue_id)) { 793 // parent email isn't associated with issue. 794 // --> create new issue, associate current email and replied email to this issue 795 $should_create_issue = true; 796 $associate_email = $reference_msg_id; 797 } else { 798 // parent email is associated with issue: 799 // --> associate current email with existing issue 800 $should_create_issue = false; 801 } 802 break; 803 } else { 804 // no matching note, email or issue: 805 // => create new issue and associate current email with it 806 $should_create_issue = true; 807 } 808 } 809 } else { 810 // - if this email is not a reply: 811 // -> create new issue and associate current email with it 812 $should_create_issue = true; 813 } 814 } 815 816 $sender_email = Mail_API::getEmailAddress($from); 817 if (PEAR::isError($sender_email)) { 818 $sender_email = 'Error Parsing Email <>'; 819 } 820 821 // only create a new issue if this email is coming from a known customer 822 if (($should_create_issue) && ($info['ema_issue_auto_creation_options']['only_known_customers'] == 'yes') && 823 (Customer::hasCustomerIntegration($info['ema_prj_id']))) { 824 list($customer_id,) = Customer::getCustomerIDByEmails($info['ema_prj_id'], array($sender_email)); 825 if (empty($customer_id)) { 826 $should_create_issue = false; 827 } 828 } 829 // check whether we need to create a new issue or not 830 if (($info['ema_issue_auto_creation'] == 'enabled') && ($should_create_issue) && (!Notification::isBounceMessage($sender_email))) { 831 $options = Email_Account::getIssueAutoCreationOptions($info['ema_id']); 832 Auth::createFakeCookie(APP_SYSTEM_USER_ID, $info['ema_prj_id']); 833 $issue_id = Issue::createFromEmail($info['ema_prj_id'], APP_SYSTEM_USER_ID, 834 $from, Mime_Helper::fixEncoding($subject), $message_body, @$options['category'], 835 $options['priority'], @$options['users'], $date, $message_id); 836 837 // add sender to authorized repliers list if they are not a real user 838 $sender_usr_id = User::getUserIDByEmail($sender_email); 839 if (empty($sender_usr_id)) { 840 Authorized_Replier::manualInsert($issue_id, $sender_email, false); 841 } 842 // associate any existing replied-to email with this new issue 843 if ((!empty($associate_email)) && (!empty($reference_issue_id))) { 844 $reference_sup_id = Support::getIDByMessageID($associate_email); 845 Support::associate(APP_SYSTEM_USER_ID, $issue_id, array($reference_sup_id)); 846 } 847 848 849 // add to and cc addresses to notification list 850 $prj_id = Auth::getCurrentProject(); 851 $project_details = Project::getDetails($prj_id); 852 $addresses_not_too_add = array($project_details['prj_outgoing_sender_email']); 853 854 if (!empty($to)) { 855 $to_addresses = Mail_API::getAddressInfo($to, true); 856 foreach ($to_addresses as $address) { 857 if ((in_array($address['email'], $addresses_not_too_add)) || (!Workflow::shouldEmailAddress($prj_id, $address['email']) || 858 (!Workflow::shouldAutoAddToNotificationList($prj_id)))) { 859 continue; 860 } 861 if (empty($address['sender_name'])) { 862 $recipient = $address['email']; 863 } else { 864 $recipient = Mail_API::getFormattedName($address['sender_name'], $address['email']); 865 } 866 Notification::subscribeEmail(Auth::getUserID(), $issue_id, $address['email'], Notification::getDefaultActions()); 867 Notification::notifyAutoCreatedIssue($prj_id, $issue_id, $from, $date, $subject, $recipient); 868 } 869 } 870 if (!empty($cc)) { 871 $cc_addresses = Mail_API::getAddressInfo($cc, true); 872 foreach ($cc_addresses as $address) { 873 if ((in_array($address['email'], $addresses_not_too_add)) || (!Workflow::shouldEmailAddress($prj_id, $address['email']) || 874 (!Workflow::shouldAutoAddToNotificationList($prj_id)))) { 875 continue; 876 } 877 if (empty($address['sender_name'])) { 878 $recipient = $address['email']; 879 } else { 880 $recipient = Mail_API::getFormattedName($address['sender_name'], $address['email']); 881 } 882 Notification::subscribeEmail(Auth::getUserID(), $issue_id, $address['email'], Notification::getDefaultActions()); 883 Notification::notifyAutoCreatedIssue($prj_id, $issue_id, $from, $date, $subject, $recipient); 884 } 885 } 886 } 887 // need to check crm for customer association 888 if (!empty($from)) { 889 $details = Email_Account::getDetails($info['ema_id']); 890 if (Customer::hasCustomerIntegration($info['ema_prj_id'])) { 891 // check for any customer contact association 892 @list($customer_id,) = Customer::getCustomerIDByEmails($info['ema_prj_id'], array($sender_email)); 893 } 894 } 895 return array( 896 'should_create_issue' => $should_create_issue, 897 'associate_email' => $associate_email, 898 'issue_id' => $issue_id, 899 'customer_id' => @$customer_id, 900 'type' => $type, 901 'parent_id' => $parent_id 902 ); 903 } 904 905 906 /** 907 * Method used to close the existing connection to the email 908 * server. 909 * 910 * @access public 911 * @param resource $mbox The mailbox 912 * @return void 913 */ 914 function closeEmailServer($mbox) 915 { 916 @imap_close($mbox); 917 } 918 919 920 /** 921 * Builds a list of all distinct message-ids available in the provided 922 * email account. 923 * 924 * @access public 925 * @param integer $ema_id The support email account ID 926 * @return array The list of message-ids 927 */ 928 function getMessageIDs($ema_id) 929 { 930 $stmt = "SELECT 931 DISTINCT sup_message_id 932 FROM 933 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email 934 WHERE 935 sup_ema_id=" . Misc::escapeInteger($ema_id); 936 $res = $GLOBALS["db_api"]->dbh->getCol($stmt); 937 if (PEAR::isError($res)) { 938 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 939 return array(); 940 } else { 941 return $res; 942 } 943 } 944 945 946 /** 947 * Checks if a message already is downloaded. 948 * 949 * @access public 950 * @param string $message_id The Message-ID header 951 * @return boolean 952 */ 953 function exists($message_id) 954 { 955 $sql = "SELECT 956 count(*) 957 FROM 958 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email 959 WHERE 960 sup_message_id = '" . Misc::escapeString($message_id) . "'"; 961 $res = $GLOBALS["db_api"]->dbh->getOne($sql); 962 if (PEAR::isError($res)) { 963 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 964 return false; 965 } 966 if ($res > 0) { 967 return true; 968 } else { 969 return false; 970 } 971 } 972 973 974 /** 975 * Method used to add a new support email to the system. 976 * 977 * @access public 978 * @param array $row The support email details 979 * @param object $structure The email structure object 980 * @param integer $sup_id The support ID to be passed out 981 * @param boolean $closing If this email comes from closing the issue 982 * @return integer 1 if the insert worked, -1 otherwise 983 */ 984 function insertEmail($row, &$structure, &$sup_id, $closing = false) 985 { 986 // get usr_id from FROM header 987 $usr_id = User::getUserIDByEmail(Mail_API::getEmailAddress($row['from'])); 988 if (!empty($usr_id) && !empty($row["customer_id"])) { 989 $row["customer_id"] = User::getCustomerID($usr_id); 990 } 991 if (empty($row['customer_id'])) { 992 $row['customer_id'] = "NULL"; 993 } 994 995 // try to get the parent ID 996 $reference_message_id = Mail_API::getReferenceMessageID($row['full_email']); 997 $parent_id = ''; 998 if (!empty($reference_message_id)) { 999 $parent_id = Support::getIDByMessageID($reference_message_id); 1000 // make sure it is in the same issue 1001 if ((!empty($parent_id)) && ((empty($row['issue_id'])) || (@$row['issue_id'] != Support::getIssueFromEmail($parent_id)))) { 1002 $parent_id = ''; 1003 } 1004 } 1005 1006 $stmt = "INSERT INTO 1007 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email 1008 ( 1009 sup_ema_id,"; 1010 if (!empty($parent_id)) { 1011 $stmt .= "\nsup_parent_id,"; 1012 } 1013 $stmt .= " 1014 sup_iss_id,"; 1015 if (!empty($usr_id)) { 1016 $stmt .= "\nsup_usr_id,\n"; 1017 } 1018 $stmt .= " sup_customer_id, 1019 sup_message_id, 1020 sup_date, 1021 sup_from, 1022 sup_to, 1023 sup_cc, 1024 sup_subject, 1025 sup_has_attachment 1026 ) VALUES ( 1027 " . Misc::escapeInteger($row["ema_id"]) . ",\n"; 1028 if (!empty($parent_id)) { 1029 $stmt .= "$parent_id,\n"; 1030 } 1031 $stmt .= Misc::escapeInteger($row["issue_id"]) . ","; 1032 if (!empty($usr_id)) { 1033 $stmt .= "\n$usr_id,\n"; 1034 } 1035 $stmt .= " 1036 " . Misc::escapeInteger($row["customer_id"]) . ", 1037 '" . Misc::escapeString($row["message_id"]) . "', 1038 '" . Misc::escapeString($row["date"]) . "', 1039 '" . Misc::escapeString($row["from"]) . "', 1040 '" . Misc::escapeString(@$row["to"]) . "', 1041 '" . Misc::escapeString(@$row["cc"]) . "', 1042 '" . Misc::escapeString($row["subject"]) . "', 1043 '" . Misc::escapeString($row["has_attachment"]) . "' 1044 )"; 1045 $res = $GLOBALS["db_api"]->dbh->query($stmt); 1046 if (PEAR::isError($res)) { 1047 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 1048 return -1; 1049 } else { 1050 $new_sup_id = $GLOBALS["db_api"]->get_last_insert_id(); 1051 $sup_id = $new_sup_id; 1052 // now add the body and full email to the separate table 1053 $stmt = "INSERT INTO 1054 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email_body 1055 ( 1056 seb_sup_id, 1057 seb_body, 1058 seb_full_email 1059 ) VALUES ( 1060 $new_sup_id, 1061 '" . Misc::escapeString($row["body"]) . "', 1062 '" . Misc::escapeString($row["full_email"]) . "' 1063 )"; 1064 $res = $GLOBALS["db_api"]->dbh->query($stmt); 1065 if (PEAR::isError($res)) { 1066 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 1067 return -1; 1068 } else { 1069 Workflow::handleNewEmail(Email_Account::getProjectID($row["ema_id"]), @$row["issue_id"], $structure, $row, $closing); 1070 return 1; 1071 } 1072 } 1073 } 1074 1075 1076 /** 1077 * Method used to get a specific parameter in the email listing 1078 * cookie. 1079 * 1080 * @access public 1081 * @param string $name The name of the parameter 1082 * @return mixed The value of the specified parameter 1083 */ 1084 function getParam($name) 1085 { 1086 if (isset($_GET[$name])) { 1087 return $_GET[$name]; 1088 } elseif (isset($_POST[$name])) { 1089 return $_POST[$name]; 1090 } elseif (($profile = Search_Profile::getProfile(Auth::getUserID(), Auth::getCurrentProject(), 'email')) && (isset($profile[$name]))) { 1091 return $profile[$name]; 1092 } else { 1093 return ""; 1094 } 1095 } 1096 1097 1098 /** 1099 * Method used to save the current search parameters in a cookie. 1100 * 1101 * @access public 1102 * @return array The search parameters 1103 */ 1104 function saveSearchParams() 1105 { 1106 $sort_by = Support::getParam('sort_by'); 1107 $sort_order = Support::getParam('sort_order'); 1108 $rows = Support::getParam('rows'); 1109 $cookie = array( 1110 'rows' => $rows ? $rows : APP_DEFAULT_PAGER_SIZE, 1111 'pagerRow' => Support::getParam('pagerRow'), 1112 'hide_associated' => Support::getParam('hide_associated'), 1113 "sort_by" => $sort_by ? $sort_by : "sup_date", 1114 "sort_order" => $sort_order ? $sort_order : "DESC", 1115 // quick filter form options 1116 'keywords' => Support::getParam('keywords'), 1117 'sender' => Support::getParam('sender'), 1118 'to' => Support::getParam('to'), 1119 'ema_id' => Support::getParam('ema_id'), 1120 'filter' => Support::getParam('filter') 1121 ); 1122 // now do some magic to properly format the date fields 1123 $date_fields = array( 1124 'arrival_date' 1125 ); 1126 foreach ($date_fields as $field_name) { 1127 $field = Support::getParam($field_name); 1128 if ((empty($field)) || ($cookie['filter'][$field_name] != 'yes')) { 1129 continue; 1130 } 1131 $end_field_name = $field_name . '_end'; 1132 $end_field = Support::getParam($end_field_name); 1133 @$cookie[$field_name] = array( 1134 'Year' => $field['Year'], 1135 'Month' => $field['Month'], 1136 'Day' => $field['Day'], 1137 'start' => $field['Year'] . '-' . $field['Month'] . '-' . $field['Day'], 1138 'filter_type' => $field['filter_type'], 1139 'end' => $end_field['Year'] . '-' . $end_field['Month'] . '-' . $end_field['Day'] 1140 ); 1141 @$cookie[$end_field_name] = array( 1142 'Year' => $end_field['Year'], 1143 'Month' => $end_field['Month'], 1144 'Day' => $end_field['Day'] 1145 ); 1146 } 1147 Search_Profile::save(Auth::getUserID(), Auth::getCurrentProject(), 'email', $cookie); 1148 return $cookie; 1149 } 1150 1151 1152 /** 1153 * Method used to get the current sorting options used in the grid 1154 * layout of the emails listing page. 1155 * 1156 * @access public 1157 * @param array $options The current search parameters 1158 * @return array The sorting options 1159 */ 1160 function getSortingInfo($options) 1161 { 1162 $fields = array( 1163 "sup_from", 1164 "sup_customer_id", 1165 "sup_date", 1166 "sup_to", 1167 "sup_iss_id", 1168 "sup_subject" 1169 ); 1170 $items = array( 1171 "links" => array(), 1172 "images" => array() 1173 ); 1174 for ($i = 0; $i < count($fields); $i++) { 1175 if ($options["sort_by"] == $fields[$i]) { 1176 $items["images"][$fields[$i]] = "images/" . strtolower($options["sort_order"]) . ".gif"; 1177 if (strtolower($options["sort_order"]) == "asc") { 1178 $sort_order = "desc"; 1179 } else { 1180 $sort_order = "asc"; 1181 } 1182 $items["links"][$fields[$i]] = $_SERVER["PHP_SELF"] . "?sort_by=" . $fields[$i] . "&sort_order=" . $sort_order; 1183 } else { 1184 $items["links"][$fields[$i]] = $_SERVER["PHP_SELF"] . "?sort_by=" . $fields[$i] . "&sort_order=asc"; 1185 } 1186 } 1187 return $items; 1188 } 1189 1190 1191 /** 1192 * Method used to get the list of emails to be displayed in the 1193 * grid layout. 1194 * 1195 * @access public 1196 * @param array $options The search parameters 1197 * @param integer $current_row The current page number 1198 * @param integer $max The maximum number of rows per page 1199 * @return array The list of issues to be displayed 1200 */ 1201 function getEmailListing($options, $current_row = 0, $max = 5) 1202 { 1203 $prj_id = Auth::getCurrentProject(); 1204 $usr_id = Auth::getUserID(); 1205 if ($max == "ALL") { 1206 $max = 9999999; 1207 } 1208 $start = $current_row * $max; 1209 1210 $stmt = "SELECT 1211 sup_id, 1212 sup_ema_id, 1213 sup_iss_id, 1214 sup_customer_id, 1215 sup_from, 1216 sup_date, 1217 sup_to, 1218 sup_subject, 1219 sup_has_attachment 1220 FROM 1221 ( 1222 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email, 1223 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "email_account"; 1224 if (!empty($options['keywords'])) { 1225 $stmt .= "," . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email_body"; 1226 } 1227 $stmt .= " 1228 ) 1229 LEFT JOIN 1230 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 1231 ON 1232 sup_iss_id = iss_id"; 1233 $stmt .= Support::buildWhereClause($options); 1234 $stmt .= " 1235 ORDER BY 1236 " . Misc::escapeString($options["sort_by"]) . " " . Misc::escapeString($options["sort_order"]); 1237 $total_rows = Pager::getTotalRows($stmt); 1238 $stmt .= " 1239 LIMIT 1240 " . Misc::escapeInteger($start) . ", " . Misc::escapeInteger($max); 1241 $res = $GLOBALS["db_api"]->dbh->getAll($stmt, DB_FETCHMODE_ASSOC); 1242 if (PEAR::isError($res)) { 1243 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 1244 return array( 1245 "list" => "", 1246 "info" => "" 1247 ); 1248 } else { 1249 if ((count($res) < 1) && ($current_row > 0)) { 1250 // if there are no results, and the page is not the first page reset page to one and reload results 1251 Auth::redirect(APP_RELATIVE_URL . "emails.php?pagerRow=0&rows=$max"); 1252 } 1253 if (Customer::hasCustomerIntegration($prj_id)) { 1254 $customer_ids = array(); 1255 for ($i = 0; $i < count($res); $i++) { 1256 if ((!empty($res[$i]['sup_customer_id'])) && (!in_array($res[$i]['sup_customer_id'], $customer_ids))) { 1257 $customer_ids[] = $res[$i]['sup_customer_id']; 1258 } 1259 } 1260 if (count($customer_ids) > 0) { 1261 $company_titles = Customer::getTitles($prj_id, $customer_ids); 1262 } 1263 } 1264 for ($i = 0; $i < count($res); $i++) { 1265 $res[$i]["sup_date"] = Date_API::getFormattedDate($res[$i]["sup_date"]); 1266 $res[$i]["sup_subject"] = Mime_Helper::fixEncoding($res[$i]["sup_subject"]); 1267 $res[$i]["sup_from"] = join(', ', Mail_API::getName($res[$i]["sup_from"], true)); 1268 if ((empty($res[$i]["sup_to"])) && (!empty($res[$i]["sup_iss_id"]))) { 1269 $res[$i]["sup_to"] = "Notification List"; 1270 } else { 1271 $to = Mail_API::getName($res[$i]["sup_to"]); 1272 # FIXME: just ignore the unformattable header? 1273 if (PEAR::isError($to)) { 1274 Error_Handler::logError(array($to->getMessage(), 'sup_id' . $res[$i]['sup_id'] . "\n" . $res[$i]["sup_to"] . "\n" . $to->getDebugInfo()), __FILE__, __LINE__); 1275 } else { 1276 $res[$i]['sup_to'] = Mime_Helper::fixEncoding($to); 1277 } 1278 } 1279 if (Customer::hasCustomerIntegration($prj_id)) { 1280 @$res[$i]['customer_title'] = $company_titles[$res[$i]['sup_customer_id']]; 1281 } 1282 } 1283 $total_pages = ceil($total_rows / $max); 1284 $last_page = $total_pages - 1; 1285 return array( 1286 "list" => $res, 1287 "info" => array( 1288 "current_page" => $current_row, 1289 "start_offset" => $start, 1290 "end_offset" => $start + count($res), 1291 "total_rows" => $total_rows, 1292 "total_pages" => $total_pages, 1293 "previous_page" => ($current_row == 0) ? "-1" : ($current_row - 1), 1294 "next_page" => ($current_row == $last_page) ? "-1" : ($current_row + 1), 1295 "last_page" => $last_page 1296 ) 1297 ); 1298 } 1299 } 1300 1301 1302 /** 1303 * Method used to get the list of emails to be displayed in the grid layout. 1304 * 1305 * @access public 1306 * @param array $options The search parameters 1307 * @return string The where clause 1308 */ 1309 function buildWhereClause($options) 1310 { 1311 $stmt = " 1312 WHERE 1313 sup_removed=0 AND 1314 sup_ema_id=ema_id AND 1315 ema_prj_id=" . Auth::getCurrentProject(); 1316 if (!empty($options["hide_associated"])) { 1317 $stmt .= " AND sup_iss_id = 0"; 1318 } 1319 if (!empty($options['keywords'])) { 1320 $stmt .= " AND sup_id=seb_sup_id "; 1321 $stmt .= " AND (" . Misc::prepareBooleanSearch('sup_subject', $options["keywords"]); 1322 $stmt .= " OR " . Misc::prepareBooleanSearch('seb_body', $options["keywords"]) . ")"; 1323 } 1324 if (!empty($options['sender'])) { 1325 $stmt .= " AND " . Misc::prepareBooleanSearch('sup_from', $options["sender"]); 1326 } 1327 if (!empty($options['to'])) { 1328 $stmt .= " AND " . Misc::prepareBooleanSearch('sup_to', $options["to"]); 1329 } 1330 if (!empty($options['ema_id'])) { 1331 $stmt .= " AND sup_ema_id=" . $options['ema_id']; 1332 } 1333 if ((!empty($options['filter'])) && ($options['filter']['arrival_date'] == 'yes')) { 1334 switch ($options['arrival_date']['filter_type']) { 1335 case 'greater': 1336 $stmt .= " AND sup_date >= '" . $options['arrival_date']['start'] . "'"; 1337 break; 1338 case 'less': 1339 $stmt .= " AND sup_date <= '" . $options['arrival_date']['start'] . "'"; 1340 break; 1341 case 'between': 1342 $stmt .= " AND sup_date BETWEEN '" . $options['arrival_date']['start'] . "' AND '" . $options['arrival_date']['end'] . "'"; 1343 break; 1344 } 1345 } 1346 1347 // handle 'private' issues. 1348 if (Auth::getCurrentRole() < User::getRoleID("Manager")) { 1349 $stmt .= " AND (iss_private = 0 OR iss_private IS NULL)"; 1350 } 1351 return $stmt; 1352 } 1353 1354 1355 /** 1356 * Method used to extract and associate attachments in an email 1357 * to the given issue. 1358 * 1359 * @access public 1360 * @param integer $issue_id The issue ID 1361 * @param mixed $input The full body of the message or decoded email. 1362 * @param boolean $internal_only Whether these files are supposed to be internal only or not 1363 * @param integer $associated_note_id The note ID that these attachments should be associated with 1364 * @return void 1365 */ 1366 function extractAttachments($issue_id, $input, $internal_only = false, $associated_note_id = false) 1367 { 1368 if (!is_object($input)) { 1369 $input = Mime_Helper::decode($input, true, true); 1370 } 1371 1372 // figure out who should be the 'owner' of this attachment 1373 $sender_email = strtolower(Mail_API::getEmailAddress($input->headers['from'])); 1374 $usr_id = User::getUserIDByEmail($sender_email); 1375 $unknown_user = false; 1376 if (empty($usr_id)) { 1377 $prj_id = Issue::getProjectID($issue_id); 1378 if (Customer::hasCustomerIntegration($prj_id)) { 1379 // try checking if a customer technical contact has this email associated with it 1380 list(,$contact_id) = Customer::getCustomerIDByEmails($prj_id, array($sender_email)); 1381 if (!empty($contact_id)) { 1382 $usr_id = User::getUserIDByContactID($contact_id); 1383 } 1384 } 1385 if (empty($usr_id)) { 1386 // if we couldn't find a real customer by that email, set the usr_id to be the system user id, 1387 // and store the actual email address in the unknown_user field. 1388 $usr_id = APP_SYSTEM_USER_ID; 1389 $unknown_user = $input->headers['from']; 1390 } 1391 } 1392 // now for the real thing 1393 $attachments = Mime_Helper::getAttachments($input); 1394 if (count($attachments) > 0) { 1395 if (empty($associated_note_id)) { 1396 $history_log = ev_gettext("Attachment originated from an email"); 1397 } else { 1398 $history_log = ev_gettext("Attachment originated from a note"); 1399 } 1400 $attachment_id = Attachment::add($issue_id, $usr_id, $history_log, $internal_only, $unknown_user, $associated_note_id); 1401 for ($i = 0; $i < count($attachments); $i++) { 1402 Attachment::addFile($attachment_id, $attachments[$i]['filename'], $attachments[$i]['filetype'], $attachments[$i]['blob']); 1403 } 1404 // mark the note as having attachments (poor man's caching system) 1405 if ($associated_note_id != false) { 1406 Note::setAttachmentFlag($associated_note_id); 1407 } 1408 } 1409 } 1410 1411 1412 /** 1413 * Method used to silently associate a support email with an 1414 * existing issue. 1415 * 1416 * @access public 1417 * @param integer $usr_id The user ID of the person performing this change 1418 * @param integer $issue_id The issue ID 1419 * @param array $items The list of email IDs to associate 1420 * @return integer 1 if it worked, -1 otherwise 1421 */ 1422 function associateEmail($usr_id, $issue_id, $items) 1423 { 1424 $stmt = "UPDATE 1425 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email 1426 SET 1427 sup_iss_id=$issue_id 1428 WHERE 1429 sup_id IN (" . @implode(", ", Misc::escapeInteger($items)) . ")"; 1430 $res = $GLOBALS["db_api"]->dbh->query($stmt); 1431 if (PEAR::isError($res)) { 1432 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 1433 return -1; 1434 } else { 1435 for ($i = 0; $i < count($items); $i++) { 1436 $full_email = Support::getFullEmail($items[$i]); 1437 Support::extractAttachments($issue_id, $full_email); 1438 } 1439 Issue::markAsUpdated($issue_id, "email"); 1440 // save a history entry for each email being associated to this issue 1441 $stmt = "SELECT 1442 sup_subject 1443 FROM 1444 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email 1445 WHERE 1446 sup_id IN (" . @implode(", ", Misc::escapeInteger($items)) . ")"; 1447 $res = $GLOBALS["db_api"]->dbh->getCol($stmt); 1448 for ($i = 0; $i < count($res); $i++) { 1449 History::add($issue_id, $usr_id, History::getTypeID('email_associated'), 1450 ev_gettext('Email (subject: \'%1$s\') associated by %2$s', $res[$i], User::getFullName($usr_id))); 1451 } 1452 return 1; 1453 } 1454 } 1455 1456 1457 /** 1458 * Method used to associate a support email with an existing 1459 * issue. 1460 * 1461 * @access public 1462 * @param integer $usr_id The user ID of the person performing this change 1463 * @param integer $issue_id The issue ID 1464 * @param array $items The list of email IDs to associate 1465 * @param boolean $authorize If the senders should be added the authorized repliers list 1466 * @return integer 1 if it worked, -1 otherwise 1467 */ 1468 function associate($usr_id, $issue_id, $items, $authorize = false, $add_recipients_to_nl = false) 1469 { 1470 $res = Support::associateEmail($usr_id, $issue_id, $items); 1471 if ($res == 1) { 1472 $stmt = "SELECT 1473 sup_id, 1474 seb_full_email 1475 FROM 1476 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email, 1477 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email_body 1478 WHERE 1479 sup_id=seb_sup_id AND 1480 sup_id IN (" . @implode(", ", Misc::escapeInteger($items)) . ")"; 1481 $res = $GLOBALS["db_api"]->dbh->getAll($stmt, DB_FETCHMODE_ASSOC); 1482 for ($i = 0; $i < count($res); $i++) { 1483 // since downloading email should make the emails 'public', send 'false' below as the 'internal_only' flag 1484 $structure = Mime_Helper::decode($res[$i]['seb_full_email'], true, false); 1485 if (Mime_Helper::hasAttachments($structure)) { 1486 $has_attachments = 1; 1487 } else { 1488 $has_attachments = 0; 1489 } 1490 $t = array( 1491 'issue_id' => $issue_id, 1492 'message_id' => @$structure->headers['message-id'], 1493 'from' => @$structure->headers['from'], 1494 'to' => @$structure->headers['to'], 1495 'cc' => @$structure->headers['cc'], 1496 'subject' => @$structure->headers['subject'], 1497 'body' => Mime_Helper::getMessageBody($structure), 1498 'full_email' => $res[$i]['seb_full_email'], 1499 'has_attachment' => $has_attachments, 1500 // the following items are not inserted, but useful in some methods 1501 'headers' => @$structure->headers 1502 ); 1503 Notification::notifyNewEmail($usr_id, $issue_id, $t, false, false, '', $res[$i]['sup_id']); 1504 if ($authorize) { 1505 Authorized_Replier::manualInsert($issue_id, Mail_API::getEmailAddress(@$structure->headers['from']), false); 1506 } 1507 } 1508 return 1; 1509 } else { 1510 return -1; 1511 } 1512 } 1513 1514 1515 /** 1516 * Method used to get the support email entry details. 1517 * 1518 * @access public 1519 * @param integer $ema_id The support email account ID 1520 * @param integer $sup_id The support email ID 1521 * @return array The email entry details 1522 */ 1523 function getEmailDetails($ema_id, $sup_id) 1524 { 1525 $stmt = "SELECT 1526 " . APP_TABLE_PREFIX . "support_email.*, 1527 " . APP_TABLE_PREFIX . "support_email_body.* 1528 FROM 1529 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email, 1530 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email_body 1531 WHERE 1532 sup_id=seb_sup_id AND 1533 sup_id=" . Misc::escapeInteger($sup_id) . " AND 1534 sup_ema_id=" . Misc::escapeInteger($ema_id); 1535 $res = $GLOBALS["db_api"]->dbh->getRow($stmt, DB_FETCHMODE_ASSOC); 1536 if (PEAR::isError($res)) { 1537 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 1538 return ""; 1539 } else { 1540 $res["attachments"] = Mime_Helper::getAttachmentCIDs($res["seb_full_email"]); 1541 $res["timestamp"] = Date_API::getUnixTimestamp($res['sup_date'], 'GMT'); 1542 $res["sup_date"] = Date_API::getFormattedDate($res["sup_date"]); 1543 $res["sup_subject"] = Mime_Helper::fixEncoding($res["sup_subject"]); 1544 $res['reply_subject'] = Mail_API::removeExcessRe('Re: ' . $res["sup_subject"], true); 1545 $res["sup_from"] = Mime_Helper::fixEncoding($res["sup_from"]); 1546 $res["sup_to"] = Mime_Helper::fixEncoding($res["sup_to"]); 1547 1548 if (!empty($res['sup_iss_id'])) { 1549 $res['reply_subject'] = Mail_API::formatSubject($res['sup_iss_id'], $res['reply_subject']); 1550 } 1551 1552 return $res; 1553 } 1554 } 1555 1556 1557 /** 1558 * Returns the nth note for a specific issue. The sequence starts at 1. 1559 * 1560 * @access public 1561 * @param integer $issue_id The id of the issue. 1562 * @param integer $sequence The sequential number of the email. 1563 * @return array An array of data containing details about the email. 1564 */ 1565 function getEmailBySequence($issue_id, $sequence) 1566 { 1567 $stmt = "SELECT 1568 sup_id, 1569 sup_ema_id 1570 FROM 1571 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email 1572 WHERE 1573 sup_iss_id = " . Misc::escapeInteger($issue_id) . " 1574 ORDER BY 1575 sup_id 1576 LIMIT " . (Misc::escapeInteger($sequence) - 1) . ", 1"; 1577 $res = $GLOBALS["db_api"]->dbh->getRow($stmt); 1578 if (PEAR::isError($res)) { 1579 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 1580 return array(); 1581 } else if (count($res) < 1) { 1582 return array(); 1583 } else { 1584 return Support::getEmailDetails($res[1], $res[0]); 1585 } 1586 } 1587 1588 1589 /** 1590 * Method used to get the list of support emails associated with 1591 * a given set of issues. 1592 * 1593 * @access public 1594 * @param array $items List of issues 1595 * @return array The list of support emails 1596 */ 1597 function getListDetails($items) 1598 { 1599 $items = @implode(", ", Misc::escapeInteger($items)); 1600 $stmt = "SELECT 1601 sup_id, 1602 sup_from, 1603 sup_subject 1604 FROM 1605 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email, 1606 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "email_account 1607 WHERE 1608 ema_id=sup_ema_id AND 1609 ema_prj_id=" . Auth::getCurrentProject() . " AND 1610 sup_id IN ($items)"; 1611 $res = $GLOBALS["db_api"]->dbh->getAll($stmt, DB_FETCHMODE_ASSOC); 1612 if (PEAR::isError($res)) { 1613 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 1614 return ""; 1615 } else { 1616 for ($i = 0; $i < count($res); $i++) { 1617 $res[$i]["sup_subject"] = Mime_Helper::fixEncoding($res[$i]["sup_subject"]); 1618 $res[$i]["sup_from"] = Mime_Helper::fixEncoding($res[$i]["sup_from"]); 1619 } 1620 return $res; 1621 } 1622 } 1623 1624 1625 /** 1626 * Method used to get the full email message for a given support 1627 * email ID. 1628 * 1629 * @access public 1630 * @param integer $sup_id The support email ID 1631 * @return string The full email message 1632 */ 1633 function getFullEmail($sup_id) 1634 { 1635 $stmt = "SELECT 1636 seb_full_email 1637 FROM 1638 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email_body 1639 WHERE 1640 seb_sup_id=" . Misc::escapeInteger($sup_id); 1641 $res = $GLOBALS["db_api"]->dbh->getOne($stmt); 1642 if (PEAR::isError($res)) { 1643 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 1644 return ""; 1645 } else { 1646 return $res; 1647 } 1648 } 1649 1650 1651 /** 1652 * Method used to get the email message for a given support 1653 * email ID. 1654 * 1655 * @access public 1656 * @param integer $sup_id The support email ID 1657 * @return string The email message 1658 */ 1659 function getEmail($sup_id) 1660 { 1661 $stmt = "SELECT 1662 seb_body 1663 FROM 1664 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email_body 1665 WHERE 1666 seb_sup_id=" . Misc::escapeInteger($sup_id); 1667 $res = $GLOBALS["db_api"]->dbh->getOne($stmt); 1668 if (PEAR::isError($res)) { 1669 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 1670 return ""; 1671 } else { 1672 return $res; 1673 } 1674 } 1675 1676 1677 /** 1678 * Method used to get all of the support email entries associated 1679 * with a given issue. 1680 * 1681 * @access public 1682 * @param integer $issue_id The issue ID 1683 * @return array The list of support emails 1684 */ 1685 function getEmailsByIssue($issue_id) 1686 { 1687 $usr_id = Auth::getUserID(); 1688 $stmt = "SELECT 1689 sup_id, 1690 sup_ema_id, 1691 sup_from, 1692 sup_to, 1693 sup_cc, 1694 sup_date, 1695 sup_subject, 1696 seb_body, 1697 sup_has_attachment, 1698 CONCAT(sup_ema_id, '-', sup_id) AS composite_id 1699 FROM 1700 ( 1701 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email, 1702 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email_body, 1703 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "email_account, 1704 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 1705 ) 1706 WHERE 1707 sup_id=seb_sup_id AND 1708 ema_id=sup_ema_id AND 1709 iss_id = sup_iss_id AND 1710 ema_prj_id=iss_prj_id AND 1711 sup_iss_id=" . Misc::escapeInteger($issue_id) . " 1712 ORDER BY 1713 sup_id ASC"; 1714 $res = $GLOBALS["db_api"]->dbh->getAll($stmt, DB_FETCHMODE_ASSOC); 1715 if (PEAR::isError($res)) { 1716 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 1717 return ""; 1718 } else { 1719 if (count($res) == 0) { 1720 return ""; 1721 } else { 1722 for ($i = 0; $i < count($res); $i++) { 1723 $res[$i]["sup_date"] = Date_API::getFormattedDate($res[$i]["sup_date"]); 1724 $res[$i]["sup_subject"] = Mime_Helper::fixEncoding($res[$i]["sup_subject"]); 1725 $res[$i]["sup_from"] = Mime_Helper::fixEncoding($res[$i]["sup_from"]); 1726 $res[$i]["sup_to"] = Mime_Helper::fixEncoding($res[$i]["sup_to"]); 1727 $res[$i]["sup_cc"] = Mime_Helper::fixEncoding($res[$i]["sup_cc"]); 1728 } 1729 return $res; 1730 } 1731 } 1732 } 1733 1734 1735 /** 1736 * Method used to update all of the selected support emails as 1737 * 'removed' ones. 1738 * 1739 * @access public 1740 * @return integer 1 if it worked, -1 otherwise 1741 */ 1742 function removeEmails() 1743 { 1744 $items = @implode(", ", Misc::escapeInteger($_POST["item"])); 1745 $stmt = "UPDATE 1746 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email 1747 SET 1748 sup_removed=1 1749 WHERE 1750 sup_id IN ($items)"; 1751 $res = $GLOBALS["db_api"]->dbh->query($stmt); 1752 if (PEAR::isError($res)) { 1753 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 1754 return -1; 1755 } else { 1756 return 1; 1757 } 1758 } 1759 1760 1761 /** 1762 * Method used to remove the association of all support emails 1763 * for a given issue. 1764 * 1765 * @access public 1766 * @return integer 1 if it worked, -1 otherwise 1767 */ 1768 function removeAssociation() 1769 { 1770 $items = @implode(", ", Misc::escapeInteger($_POST["item"])); 1771 $stmt = "SELECT 1772 sup_iss_id 1773 FROM 1774 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email 1775 WHERE 1776 sup_id IN ($items)"; 1777 $issue_id = $GLOBALS["db_api"]->dbh->getOne($stmt); 1778 1779 $stmt = "UPDATE 1780 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email 1781 SET 1782 sup_iss_id=0 1783 WHERE 1784 sup_id IN ($items)"; 1785 $res = $GLOBALS["db_api"]->dbh->query($stmt); 1786 if (PEAR::isError($res)) { 1787 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 1788 return -1; 1789 } else { 1790 Issue::markAsUpdated($issue_id); 1791 // save a history entry for each email being associated to this issue 1792 $stmt = "SELECT 1793 sup_id, 1794 sup_subject 1795 FROM 1796 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email 1797 WHERE 1798 sup_id IN ($items)"; 1799 $subjects = $GLOBALS["db_api"]->dbh->getAssoc($stmt); 1800 for ($i = 0; $i < count($_POST["item"]); $i++) { 1801 History::add($issue_id, Auth::getUserID(), History::getTypeID('email_disassociated'), 1802 ev_gettext('Email (subject: \'%1$s\') disassociated by %2$s', $subjects[$_POST["item"][$i]], User::getFullName(Auth::getUserID()))); 1803 } 1804 return 1; 1805 } 1806 } 1807 1808 1809 /** 1810 * Checks whether the given email address is allowed to send emails in the 1811 * issue ID. 1812 * 1813 * @access public 1814 * @param integer $issue_id The issue ID 1815 * @param string $sender_email The email address 1816 * @return boolean 1817 */ 1818 function isAllowedToEmail($issue_id, $sender_email) 1819 { 1820 $prj_id = Issue::getProjectID($issue_id); 1821 1822 // check the workflow 1823 $workflow_can_email = Workflow::canEmailIssue($prj_id, $issue_id, $sender_email); 1824 if ($workflow_can_email != null) { 1825 return $workflow_can_email; 1826 } 1827 1828 $is_allowed = true; 1829 $sender_usr_id = User::getUserIDByEmail($sender_email); 1830 if (empty($sender_usr_id)) { 1831 if (Customer::hasCustomerIntegration($prj_id)) { 1832 // check for a customer contact with several email addresses 1833 $customer_id = Issue::getCustomerID($issue_id); 1834 $contact_emails = array_keys(Customer::getContactEmailAssocList($prj_id, $customer_id)); 1835 $contact_emails = array_map('strtolower', $contact_emails); 1836 if ((!in_array(strtolower($sender_email), $contact_emails)) && 1837 (!Authorized_Replier::isAuthorizedReplier($issue_id, $sender_email))) { 1838 $is_allowed = false; 1839 } 1840 } else { 1841 if (!Authorized_Replier::isAuthorizedReplier($issue_id, $sender_email)) { 1842 $is_allowed = false; 1843 } 1844 } 1845 } else { 1846 // check if this user is not a customer and 1847 // also not in the assignment list for the current issue and 1848 // also not in the authorized repliers list 1849 // also not the reporter 1850 $details = Issue::getDetails($issue_id); 1851 if (!Issue::canAccess($issue_id, $sender_usr_id)) { 1852 $is_allowed = false; 1853 } if (($sender_usr_id != $details['iss_usr_id']) && 1854 (!Authorized_Replier::isUserAuthorizedReplier($issue_id, $sender_usr_id)) && 1855 (!Issue::isAssignedToUser($issue_id, $sender_usr_id)) && 1856 (User::getRoleByUser($sender_usr_id, Issue::getProjectID($issue_id)) != User::getRoleID('Customer'))) { 1857 $is_allowed = false; 1858 } elseif ((User::getRoleByUser($sender_usr_id, Issue::getProjectID($issue_id)) == User::getRoleID('Customer')) && 1859 (User::getCustomerID($sender_usr_id) != Issue::getCustomerID($issue_id))) { 1860 $is_allowed = false; 1861 } 1862 } 1863 return $is_allowed; 1864 } 1865 1866 1867 /** 1868 * Method used to build the headers of a web-based message. 1869 * 1870 * @access public 1871 * @param integer $issue_id The issue ID 1872 * @param string $message_id The message-id 1873 * @param string $from The sender of this message 1874 * @param string $to The primary recipient of this message 1875 * @param string $cc The extra recipients of this message 1876 * @param string $body The message body 1877 * @param string $in_reply_to The message-id that we are replying to 1878 * @return string The full email 1879 */ 1880 function buildFullHeaders($issue_id, $message_id, $from, $to, $cc, $subject, $body, $in_reply_to) 1881 { 1882 // hack needed to get the full headers of this web-based email 1883 $mail = new Mail_API; 1884 $mail->setTextBody($body); 1885 if (!empty($issue_id)) { 1886 $mail->setHeaders(array("Message-Id" => $message_id)); 1887 } else { 1888 $issue_id = 0; 1889 } 1890 1891 // if there is no existing in-reply-to header, get the root message for the issue 1892 if (($in_reply_to == false) && (!empty($issue_id))) { 1893 $in_reply_to = Issue::getRootMessageID($issue_id); 1894 } 1895 1896 if ($in_reply_to) { 1897 $mail->setHeaders(array("In-Reply-To" => $in_reply_to)); 1898 } 1899 $cc = trim($cc); 1900 if (!empty($cc)) { 1901 $cc = str_replace(",", ";", $cc); 1902 $ccs = explode(";", $cc); 1903 for ($i = 0; $i < count($ccs); $i++) { 1904 if (!empty($ccs[$i])) { 1905 $mail->addCc($ccs[$i]); 1906 } 1907 } 1908 } 1909 return $mail->getFullHeaders($from, $to, $subject); 1910 } 1911 1912 1913 /** 1914 * Method used to send emails directly from the sender to the 1915 * recipient. This will not re-write the sender's email address 1916 * to issue-xxxx@ or whatever. 1917 * 1918 * @access public 1919 * @param integer $issue_id The issue ID 1920 * @param string $from The sender of this message 1921 * @param string $to The primary recipient of this message 1922 * @param string $cc The extra recipients of this message 1923 * @param string $subject The subject of this message 1924 * @param string $body The message body 1925 * @param string $message_id The message-id 1926 * @param integer $sender_usr_id The ID of the user sending this message. 1927 * @return void 1928 */ 1929 function sendDirectEmail($issue_id, $from, $to, $cc, $subject, $body, $message_id, $sender_usr_id = false) 1930 { 1931 $subject = Mail_API::formatSubject($issue_id, $subject); 1932 $recipients = Support::getRecipientsCC($cc); 1933 $recipients[] = $to; 1934 // send the emails now, one at a time 1935 foreach ($recipients as $recipient) { 1936 $mail = new Mail_API; 1937 if (!empty($issue_id)) { 1938 // add the warning message to the current message' body, if needed 1939 $fixed_body = Mail_API::addWarningMessage($issue_id, $recipient, $body, array()); 1940 $mail->setHeaders(array( 1941 "Message-Id" => $message_id 1942 )); 1943 // skip users who don't have access to this issue 1944 $recipient_usr_id = User::getUserIDByEmail(Mail_API::getEmailAddress($recipient)); 1945 if (((!empty($recipient_usr_id)) && (!Issue::canAccess($issue_id, $recipient_usr_id))) || 1946 (empty($recipient_usr_id)) && (Issue::isPrivate($issue_id))) { 1947 continue; 1948 } 1949 } else { 1950 $fixed_body = $body; 1951 } 1952 if (User::getRoleByUser(User::getUserIDByEmail(Mail_API::getEmailAddress($from)), Issue::getProjectID($issue_id)) == User::getRoleID("Customer")) { 1953 $type = 'customer_email'; 1954 } else { 1955 $type = 'other_email'; 1956 } 1957 $mail->setTextBody($fixed_body); 1958 $mail->send($from, $recipient, $subject, TRUE, $issue_id, $type, $sender_usr_id); 1959 } 1960 } 1961 1962 1963 /** 1964 * Method used to parse the Cc list in a string format and return 1965 * an array of the email addresses contained within. 1966 * 1967 * @access public 1968 * @param string $cc The Cc list 1969 * @return array The list of email addresses 1970 */ 1971 function getRecipientsCC($cc) 1972 { 1973 $cc = trim($cc); 1974 if (empty($cc)) { 1975 return array(); 1976 } else { 1977 $cc = str_replace(",", ";", $cc); 1978 return explode(";", $cc); 1979 } 1980 } 1981 1982 1983 /** 1984 * Method used to send an email from the user interface. 1985 * 1986 * @access public 1987 * @return integer 1 if it worked, -1 otherwise 1988 */ 1989 function sendEmail($parent_sup_id = FALSE) 1990 { 1991 // if we are replying to an existing email, set the In-Reply-To: header accordingly 1992 if ($parent_sup_id) { 1993 $in_reply_to = Support::getMessageIDByID($parent_sup_id); 1994 } else { 1995 $in_reply_to = false; 1996 } 1997 1998 // get ID of whoever is sending this. 1999 $sender_usr_id = User::getUserIDByEmail(Mail_API::getEmailAddress($_POST["from"])); 2000 if (empty($sender_usr_id)) { 2001 $sender_usr_id = false; 2002 } 2003 2004 // get type of email this is 2005 if (!empty($_POST['type'])) { 2006 $type = $_POST['type']; 2007 } else { 2008 $type = ''; 2009 } 2010 2011 2012 // remove extra 'Re: ' from subject 2013 $_POST['subject'] = Mail_API::removeExcessRe($_POST['subject'], true); 2014 $internal_only = false; 2015 $message_id = Mail_API::generateMessageID(); 2016 // hack needed to get the full headers of this web-based email 2017 $full_email = Support::buildFullHeaders($_POST["issue_id"], $message_id, $_POST["from"], 2018 $_POST["to"], $_POST["cc"], $_POST["subject"], $_POST["message"], $in_reply_to); 2019 2020 // email blocking should only be done if this is an email about an associated issue 2021 if (!empty($_POST['issue_id'])) { 2022 $user_info = User::getNameEmail(Auth::getUserID()); 2023 // check whether the current user is allowed to send this email to customers or not 2024 if (!Support::isAllowedToEmail($_POST["issue_id"], $user_info['usr_email'])) { 2025 // add the message body as a note 2026 $_POST['blocked_msg'] = $full_email; 2027 $_POST['title'] = $_POST["subject"]; 2028 $_POST['note'] = Mail_API::getCannedBlockedMsgExplanation() . $_POST["message"]; 2029 Note::insert(Auth::getUserID(), $_POST["issue_id"]); 2030 Workflow::handleBlockedEmail(Issue::getProjectID($_POST['issue_id']), $_POST['issue_id'], $_POST, 'web'); 2031 return 1; 2032 } 2033 } 2034 2035 // only send a direct email if the user doesn't want to add the Cc'ed people to the notification list 2036 if (@$_POST['add_unknown'] == 'yes') { 2037 if (!empty($_POST['issue_id'])) { 2038 // add the recipients to the notification list of the associated issue 2039 $recipients = array($_POST['to']); 2040 $recipients = array_merge($recipients, Support::getRecipientsCC($_POST['cc'])); 2041 for ($i = 0; $i < count($recipients); $i++) { 2042 if ((!empty($recipients[$i])) && (!Notification::isIssueRoutingSender($_POST["issue_id"], $recipients[$i]))) { 2043 Notification::subscribeEmail(Auth::getUserID(), $_POST["issue_id"], Mail_API::getEmailAddress($recipients[$i]), Notification::getDefaultActions()); 2044 } 2045 } 2046 } 2047 } else { 2048 // Usually when sending out emails associated to an issue, we would 2049 // simply insert the email in the table and call the Notification::notifyNewEmail() method, 2050 // but on this case we need to actually send the email to the recipients that are not 2051 // already in the notification list for the associated issue, if any. 2052 // In the case of replying to an email that is not yet associated with an issue, then 2053 // we are always directly sending the email, without using any notification list 2054 // functionality. 2055 if (!empty($_POST['issue_id'])) { 2056 // send direct emails only to the unknown addresses, and leave the rest to be 2057 // catched by the notification list 2058 $from = Notification::getFixedFromHeader($_POST['issue_id'], $_POST['from'], 'issue'); 2059 // build the list of unknown recipients 2060 if (!empty($_POST['to'])) { 2061 $recipients = array($_POST['to']); 2062 $recipients = array_merge($recipients, Support::getRecipientsCC($_POST['cc'])); 2063 } else { 2064 $recipients = Support::getRecipientsCC($_POST['cc']); 2065 } 2066 $unknowns = array(); 2067 for ($i = 0; $i < count($recipients); $i++) { 2068 if (!Notification::isSubscribedToEmails($_POST['issue_id'], $recipients[$i])) { 2069 $unknowns[] = $recipients[$i]; 2070 } 2071 } 2072 if (count($unknowns) > 0) { 2073 $to = array_shift($unknowns); 2074 $cc = implode('; ', $unknowns); 2075 // send direct emails 2076 Support::sendDirectEmail($_POST['issue_id'], $from, $to, $cc, 2077 $_POST['subject'], $_POST['message'], $message_id, $sender_usr_id); 2078 } 2079 } else { 2080 // send direct emails to all recipients, since we don't have an associated issue 2081 $project_info = Project::getOutgoingSenderAddress(Auth::getCurrentProject()); 2082 // use the project-related outgoing email address, if there is one 2083 if (!empty($project_info['email'])) { 2084 $from = Mail_API::getFormattedName(User::getFullName(Auth::getUserID()), $project_info['email']); 2085 } else { 2086 // otherwise, use the real email address for the current user 2087 $from = User::getFromHeader(Auth::getUserID()); 2088 } 2089 // send direct emails 2090 Support::sendDirectEmail($_POST['issue_id'], $from, $_POST['to'], $_POST['cc'], 2091 $_POST['subject'], $_POST['message'], $message_id); 2092 } 2093 } 2094 2095 $t = array( 2096 'customer_id' => 'NULL', 2097 'issue_id' => $_POST["issue_id"] ? $_POST["issue_id"] : 0, 2098 'ema_id' => $_POST['ema_id'], 2099 'message_id' => $message_id, 2100 'date' => Date_API::getCurrentDateGMT(), 2101 'from' => $_POST['from'], 2102 'to' => $_POST['to'], 2103 'cc' => @$_POST['cc'], 2104 'subject' => @$_POST['subject'], 2105 'body' => $_POST['message'], 2106 'full_email' => $full_email, 2107 'has_attachment' => 0 2108 ); 2109 // associate this new email with a customer, if appropriate 2110 if (Auth::getCurrentRole() == User::getRoleID('Customer')) { 2111 $customer_id = User::getCustomerID(Auth::getUserID()); 2112 if ((!empty($customer_id)) && ($customer_id != -1)) { 2113 $t['customer_id'] = $customer_id; 2114 } 2115 } 2116 $structure = Mime_Helper::decode($full_email, true, false); 2117 $t['headers'] = $structure->headers; 2118 $res = Support::insertEmail($t, $structure, $sup_id); 2119 if (!empty($_POST["issue_id"])) { 2120 // need to send a notification 2121 Notification::notifyNewEmail(Auth::getUserID(), $_POST["issue_id"], $t, $internal_only, false, $type, $sup_id); 2122 // mark this issue as updated 2123 if ((!empty($t['customer_id'])) && ($t['customer_id'] != 'NULL')) { 2124 Issue::markAsUpdated($_POST["issue_id"], 'customer action'); 2125 } else { 2126 if ((!empty($sender_usr_id)) && (User::getRoleByUser($sender_usr_id, Issue::getProjectID($_POST['issue_id'])) > User::getRoleID('Customer'))) { 2127 Issue::markAsUpdated($_POST["issue_id"], 'staff response'); 2128 } else { 2129 Issue::markAsUpdated($_POST["issue_id"], 'user response'); 2130 } 2131 } 2132 // save a history entry for this 2133 History::add($_POST["issue_id"], Auth::getUserID(), History::getTypeID('email_sent'), 2134 ev_gettext('Outgoing email sent by %1$s', User::getFullName(Auth::getUserID()))); 2135 } 2136 2137 return 1; 2138 } 2139 2140 2141 /** 2142 * Method used to get the message-id associated with a given support 2143 * email entry. 2144 * 2145 * @access public 2146 * @param integer $sup_id The support email ID 2147 * @return integer The email ID 2148 */ 2149 function getMessageIDByID($sup_id) 2150 { 2151 $stmt = "SELECT 2152 sup_message_id 2153 FROM 2154 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email 2155 WHERE 2156 sup_id=" . Misc::escapeInteger($sup_id); 2157 $res = $GLOBALS["db_api"]->dbh->getOne($stmt); 2158 if (PEAR::isError($res)) { 2159 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 2160 return ""; 2161 } else { 2162 return $res; 2163 } 2164 } 2165 2166 2167 /** 2168 * Method used to get the support ID associated with a given support 2169 * email message-id. 2170 * 2171 * @access public 2172 * @param string $message_id The message ID 2173 * @return integer The email ID 2174 */ 2175 function getIDByMessageID($message_id) 2176 { 2177 $stmt = "SELECT 2178 sup_id 2179 FROM 2180 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email 2181 WHERE 2182 sup_message_id='" . Misc::escapeString($message_id) . "'"; 2183 $res = $GLOBALS["db_api"]->dbh->getOne($stmt); 2184 if (PEAR::isError($res)) { 2185 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 2186 return false; 2187 } else { 2188 if (empty($res)) { 2189 return false; 2190 } else { 2191 return $res; 2192 } 2193 } 2194 } 2195 2196 2197 /** 2198 * Method used to get the issue ID associated with a given support 2199 * email message-id. 2200 * 2201 * @access public 2202 * @param string $message_id The message ID 2203 * @return integer The issue ID 2204 */ 2205 function getIssueByMessageID($message_id) 2206 { 2207 $stmt = "SELECT 2208 sup_iss_id 2209 FROM 2210 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email 2211 WHERE 2212 sup_message_id='" . Misc::escapeString($message_id) . "'"; 2213 $res = $GLOBALS["db_api"]->dbh->getOne($stmt); 2214 if (PEAR::isError($res)) { 2215 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 2216 return ""; 2217 } else { 2218 return $res; 2219 } 2220 } 2221 2222 2223 /** 2224 * Method used to get the issue ID associated with a given support 2225 * email entry. 2226 * 2227 * @access public 2228 * @param integer $sup_id The support email ID 2229 * @return integer The issue ID 2230 */ 2231 function getIssueFromEmail($sup_id) 2232 { 2233 $stmt = "SELECT 2234 sup_iss_id 2235 FROM 2236 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email 2237 WHERE 2238 sup_id=" . Misc::escapeInteger($sup_id); 2239 $res = $GLOBALS["db_api"]->dbh->getOne($stmt); 2240 if (PEAR::isError($res)) { 2241 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 2242 return ""; 2243 } else { 2244 return $res; 2245 } 2246 } 2247 2248 2249 /** 2250 * Returns the message-id of the parent email. 2251 * 2252 * @access public 2253 * @param string $msg_id The message ID 2254 * @return string The message id of the parent email or false 2255 */ 2256 function getParentMessageIDbyMessageID($msg_id) 2257 { 2258 $sql = "SELECT 2259 parent.sup_message_id 2260 FROM 2261 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email child, 2262 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email parent 2263 WHERE 2264 parent.sup_id = child.sup_parent_id AND 2265 child.sup_message_id = '" . Misc::escapeString($msg_id) . "'"; 2266 $res = $GLOBALS["db_api"]->dbh->getOne($sql); 2267 if (PEAR::isError($res)) { 2268 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 2269 return false; 2270 } else { 2271 if (empty($res)) { 2272 return false; 2273 } 2274 return $res; 2275 } 2276 2277 } 2278 2279 2280 /** 2281 * Returns the number of emails sent by a user in a time range. 2282 * 2283 * @access public 2284 * @param string $usr_id The ID of the user 2285 * @param integer $start The timestamp of the start date 2286 * @param integer $end The timestanp of the end date 2287 * @param boolean $associated If this should return emails associated with issues or non associated emails. 2288 * @return integer The number of emails sent by the user. 2289 */ 2290 function getSentEmailCountByUser($usr_id, $start, $end, $associated) 2291 { 2292 $usr_info = User::getNameEmail($usr_id); 2293 $stmt = "SELECT 2294 COUNT(sup_id) 2295 FROM 2296 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email, 2297 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "email_account 2298 WHERE 2299 ema_id = sup_ema_id AND 2300 ema_prj_id = " . Auth::getCurrentProject() . " AND 2301 sup_date BETWEEN '" . Misc::escapeString($start) . "' AND '" . Misc::escapeString($end) . "' AND 2302 sup_from LIKE '%" . Misc::escapeString($usr_info["usr_email"]) . "%' AND 2303 sup_iss_id "; 2304 if ($associated == true) { 2305 $stmt .= "!= 0"; 2306 } else { 2307 $stmt .= "= 0"; 2308 } 2309 $res = $GLOBALS["db_api"]->dbh->getOne($stmt); 2310 if (PEAR::isError($res)) { 2311 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 2312 return ""; 2313 } 2314 return $res; 2315 } 2316 2317 2318 /** 2319 * Returns the projectID based on the email account 2320 * 2321 * @access public 2322 * @param integer $ema_id The id of the email account. 2323 * @return integer The ID of the of the project. 2324 */ 2325 function getProjectByEmailAccount($ema_id) 2326 { 2327 static $returns; 2328 2329 if (!empty($returns[$ema_id])) { 2330 return $returns[$ema_id]; 2331 } 2332 2333 $stmt = "SELECT 2334 ema_prj_id 2335 FROM 2336 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "email_account 2337 WHERE 2338 ema_id = " . Misc::escapeInteger($ema_id); 2339 $res = $GLOBALS["db_api"]->dbh->getOne($stmt); 2340 if (PEAR::isError($res)) { 2341 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 2342 return -1; 2343 } 2344 $returns[$ema_id] = $res; 2345 return $res; 2346 } 2347 2348 2349 /** 2350 * Moves an email from one account to another. 2351 * 2352 * @access public 2353 * @param integer $sup_id The ID of the message. 2354 * @param integer $current_ema_id The ID of the account the message is currently in. 2355 * @param integer $new_ema_id The ID of the account to move the message too. 2356 * @return integer -1 if there was error moving the message, 1 otherwise. 2357 */ 2358 function moveEmail($sup_id, $current_ema_id, $new_ema_id) 2359 { 2360 $usr_id = Auth::getUserID(); 2361 $email = Support::getEmailDetails($current_ema_id, $sup_id); 2362 if (!empty($email['sup_iss_id'])) { 2363 return -1; 2364 } 2365 2366 $info = Email_Account::getDetails($new_ema_id); 2367 $full_email = Support::getFullEmail($sup_id); 2368 $structure = Mime_Helper::decode($full_email, true, true); 2369 $headers = ''; 2370 foreach ($structure->headers as $key => $value) { 2371 if (is_array($value)) { 2372 continue; 2373 } 2374 $headers .= "$key: $value\n"; 2375 } 2376 2377 // handle auto creating issues (if needed) 2378 $should_create_array = Support::createIssueFromEmail($info, $headers, $email['seb_body'], $email['timestamp'], $email['sup_from'], $email['sup_subject']); 2379 $should_create_issue = $should_create_array['should_create_issue']; 2380 $associate_email = $should_create_array['associate_email']; 2381 $issue_id = $should_create_array['issue_id']; 2382 $customer_id = $should_create_array['customer_id']; 2383 2384 if (empty($issue_id)) { 2385 $issue_id = 0; 2386 } 2387 if (empty($customer_id)) { 2388 $customer_id = 'NULL'; 2389 } 2390 2391 $sql = "UPDATE 2392 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email 2393 SET 2394 sup_ema_id = " . Misc::escapeInteger($new_ema_id) . ", 2395 sup_iss_id = " . Misc::escapeInteger($issue_id) . ", 2396 sup_customer_id = " . Misc::escapeInteger($customer_id) . " 2397 WHERE 2398 sup_id = " . Misc::escapeInteger($sup_id) . " AND 2399 sup_ema_id = " . Misc::escapeInteger($current_ema_id); 2400 $res = $GLOBALS["db_api"]->dbh->query($sql); 2401 if (PEAR::isError($res)) { 2402 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 2403 return -1; 2404 } 2405 2406 $row = array( 2407 'customer_id' => $customer_id, 2408 'issue_id' => $issue_id, 2409 'ema_id' => $new_ema_id, 2410 'message_id' => $email['sup_message_id'], 2411 'date' => $email['timestamp'], 2412 'from' => $email['sup_from'], 2413 'to' => $email['sup_to'], 2414 'cc' => $email['sup_cc'], 2415 'subject' => $email['sup_subject'], 2416 'body' => $email['seb_body'], 2417 'full_email' => $email['seb_full_email'], 2418 'has_attachment' => $email['sup_has_attachment'] 2419 ); 2420 Workflow::handleNewEmail(Support::getProjectByEmailAccount($new_ema_id), $issue_id, $structure, $row); 2421 return 1; 2422 } 2423 2424 2425 /** 2426 * Deletes the specified message from the server 2427 * NOTE: YOU STILL MUST call imap_expunge($mbox) to permanently delete the message. 2428 * 2429 * @param array $info An array of email account information 2430 * @param object $mbox The mailbox object 2431 * @param integer $num The number of the message to delete. 2432 */ 2433 function deleteMessage($info, $mbox, $num) 2434 { 2435 // need to delete the message from the server? 2436 if (!$info['ema_leave_copy']) { 2437 @imap_delete($mbox, $num); 2438 } else { 2439 // mark the message as already read 2440 @imap_setflag_full($mbox, $num, "\\Seen"); 2441 } 2442 } 2443 2444 2445 /** 2446 * Check if this email needs to be blocked and if so, block it. 2447 * 2448 * 2449 */ 2450 function blockEmailIfNeeded($email) 2451 { 2452 if (empty($email['issue_id'])) { 2453 return false; 2454 } 2455 2456 $issue_id = $email['issue_id']; 2457 $prj_id = Issue::getProjectID($issue_id); 2458 $sender_email = strtolower(Mail_API::getEmailAddress($email['headers']['from'])); 2459 list($text_headers, $body) = Mime_Helper::splitHeaderBody($email['full_email']); 2460 if ((Mail_API::isVacationAutoResponder($email['headers'])) || (Notification::isBounceMessage($sender_email)) || 2461 (!Support::isAllowedToEmail($issue_id, $sender_email))) { 2462 // add the message body as a note 2463 $_POST = array( 2464 'blocked_msg' => $email['full_email'], 2465 'title' => @$email['headers']['subject'], 2466 'note' => Mail_API::getCannedBlockedMsgExplanation($issue_id) . $email['body'], 2467 'message_id' => Mail_API::getMessageID($text_headers, $body), 2468 ); 2469 // avoid having this type of message re-open the issue 2470 if (Mail_API::isVacationAutoResponder($email['headers'])) { 2471 $closing = true; 2472 $notify = false; 2473 } else { 2474 $closing = false; 2475 $notify = true; 2476 } 2477 $res = Note::insert(Auth::getUserID(), $issue_id, $email['headers']['from'], false, $closing, $notify); 2478 // associate the email attachments as internal-only files on this issue 2479 if ($res != -1) { 2480 Support::extractAttachments($issue_id, $email['full_email'], true, $res); 2481 } 2482 2483 $_POST['issue_id'] = $issue_id; 2484 $_POST['from'] = $sender_email; 2485 2486 // avoid having this type of message re-open the issue 2487 if (Mail_API::isVacationAutoResponder($email['headers'])) { 2488 $email_type = 'vacation-autoresponder'; 2489 } else { 2490 $email_type = 'routed'; 2491 } 2492 Workflow::handleBlockedEmail($prj_id, $issue_id, $_POST, $email_type); 2493 2494 // try to get usr_id of sender, if not, use system account 2495 $usr_id = User::getUserIDByEmail(Mail_API::getEmailAddress($email['from'])); 2496 if (!$usr_id) { 2497 $usr_id = APP_SYSTEM_USER_ID; 2498 } 2499 // log blocked email 2500 History::add($issue_id, $usr_id, History::getTypeID('email_blocked'), ev_gettext('Email from \'%1$s\' blocked', $email['from'])); 2501 return true; 2502 } 2503 return false; 2504 } 2505 } 2506 2507 // benchmarking the included file (aka setup time) 2508 if (APP_BENCHMARK) { 2509 $GLOBALS['bench']->setMarker('Included Support Class'); 2510 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Dec 19 21:21:33 2007 | Cross-referenced by PHPXref 0.7 |