[ Index ]

PHP Cross Reference of Eventum

title

Body

[close]

/include/ -> class.routing.php (source)

   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: Bryan Alsdorf <bryan@mysql.com>                             |
  26  // +----------------------------------------------------------------------+
  27  //
  28  
  29  require_once (APP_INC_PATH . "class.misc.php");
  30  require_once (APP_INC_PATH . "class.mail.php");
  31  require_once (APP_INC_PATH . "class.support.php");
  32  require_once (APP_INC_PATH . "class.issue.php");
  33  require_once (APP_INC_PATH . "class.mime_helper.php");
  34  require_once (APP_INC_PATH . "class.date.php");
  35  require_once (APP_INC_PATH . "class.setup.php");
  36  require_once (APP_INC_PATH . "class.notification.php");
  37  require_once (APP_INC_PATH . "class.user.php");
  38  require_once (APP_INC_PATH . "class.note.php");
  39  require_once (APP_INC_PATH . "class.project.php");
  40  require_once (APP_INC_PATH . "class.status.php");
  41  require_once (APP_INC_PATH . "class.history.php");
  42  
  43  /**
  44   * Class to handle all routing functionality
  45   *
  46   * @author  Bryan Alsdorf <bryan@mysql.com>
  47   * @version 1.0
  48   */
  49  class Routing
  50  {
  51      /**
  52       * Routes an email to the correct issue.
  53       *
  54       * @param   string $full_message The full email message, including headers
  55       * @return  mixed   true or array(ERROR_CODE, ERROR_STRING) in case of failure
  56       */
  57      function route_emails($full_message)
  58      {
  59          // need some validation here
  60          if (empty($full_message)) {
  61              return array(66, ev_gettext("Error: The email message was empty") . ".\n");
  62          }
  63  
  64          // save the full message for logging purposes
  65          Support::saveRoutedEmail($full_message);
  66  
  67          // check if the email routing interface is even supposed to be enabled
  68          $setup = Setup::load();
  69          if ($setup['email_routing']['status'] != 'enabled') {
  70              return array(78, ev_gettext("Error: The email routing interface is disabled.") . "\n");
  71          }
  72          if (empty($setup['email_routing']['address_prefix'])) {
  73              return array(78, ev_gettext("Error: Please configure the email address prefix.") . "\n");
  74          }
  75          if (empty($setup['email_routing']['address_host'])) {
  76              return array(78, ev_gettext("Error: Please configure the email address domain.") . "\n");
  77          }
  78  
  79          // associate routed emails to the internal system account
  80          $sys_account = User::getNameEmail(APP_SYSTEM_USER_ID);
  81          if (empty($sys_account['usr_email'])) {
  82              return array(78, ev_gettext("Error: The associated user for the email routing interface needs to be set.") . "\n");
  83          }
  84          unset($sys_account);
  85  
  86          // join the Content-Type line (for easier parsing?)
  87          if (preg_match('/^boundary=/m', $full_message)) {
  88              $pattern = "#(Content-Type: multipart/.+); ?\r?\n(boundary=.*)$#im";
  89              $replacement = '$1; $2';
  90              $full_message = preg_replace($pattern, $replacement, $full_message);
  91          }
  92  
  93          // remove the reply-to: header
  94          if (preg_match('/^reply-to:.*/im', $full_message)) {
  95              $full_message = preg_replace("/^(reply-to:).*\n/im", '', $full_message, 1);
  96          }
  97  
  98          Auth::createFakeCookie(APP_SYSTEM_USER_ID);
  99  
 100          $structure = Mime_Helper::decode($full_message, true, true);
 101  
 102          // find which issue ID this email refers to
 103          if (isset($structure->headers['to'])) {
 104              $issue_id = Routing::getMatchingIssueIDs($structure->headers['to'], 'email');
 105          }
 106          // we need to try the Cc header as well
 107          if (empty($issue_id) and isset($structure->headers['cc'])) {
 108              $issue_id = Routing::getMatchingIssueIDs($structure->headers['cc'], 'email');
 109          }
 110  
 111          if (empty($issue_id)) {
 112              return array(65, ev_gettext("Error: The routed email had no associated Eventum issue ID or had an invalid recipient address.") . "\n");
 113          }
 114  
 115          $issue_prj_id = Issue::getProjectID($issue_id);
 116          if (empty($issue_prj_id)) {
 117              return array(65, ev_gettext("Error: The routed email had no associated Eventum issue ID or had an invalid recipient address.") . "\n");
 118          }
 119  
 120          $email_account_id = Email_Account::getEmailAccount($issue_prj_id);
 121          if (empty($email_account_id)) {
 122              return array(78, ev_gettext("Error: Please provide the email account ID.") . "\n");
 123          }
 124  
 125          $body = Mime_Helper::getMessageBody($structure);
 126  
 127          // hack for clients that set more then one from header
 128          if (is_array($structure->headers['from'])) {
 129              $structure->headers['from'] = $structure->headers['from'][0];
 130          }
 131  
 132          // associate the email to the issue
 133          $parts = array();
 134          Mime_Helper::parse_output($structure, $parts);
 135  
 136          // get the sender's email address
 137          $sender_email = strtolower(Mail_API::getEmailAddress($structure->headers['from']));
 138  
 139          // strip out the warning message sent to staff users
 140          if (($setup['email_routing']['status'] == 'enabled') &&
 141                  ($setup['email_routing']['warning']['status'] == 'enabled')) {
 142              $full_message = Mail_API::stripWarningMessage($full_message);
 143              $body = Mail_API::stripWarningMessage($body);
 144          }
 145  
 146          $prj_id = Issue::getProjectID($issue_id);
 147          Auth::createFakeCookie(APP_SYSTEM_USER_ID, $prj_id);
 148  
 149          if (Mime_Helper::hasAttachments($structure)) {
 150              $has_attachments = 1;
 151          } else {
 152              $has_attachments = 0;
 153          }
 154  
 155          // remove certain CC addresses
 156          if ((!empty($structure->headers['cc'])) && (@$setup['smtp']['save_outgoing_email'] == 'yes')) {
 157              $ccs = explode(",", @$structure->headers['cc']);
 158              for ($i = 0; $i < count($ccs); $i++) {
 159                  if (Mail_API::getEmailAddress($ccs[$i]) == $setup['smtp']['save_address']) {
 160                      unset($ccs[$i]);
 161                  }
 162              }
 163              @$structure->headers['cc'] = join(', ', $ccs);
 164          }
 165  
 166          // Remove excess Re's
 167          @$structure->headers['subject'] = Mail_API::removeExcessRe(@$structure->headers['subject'], true);
 168  
 169          $t = array(
 170              'issue_id'       => $issue_id,
 171              'ema_id'         => $email_account_id,
 172              'message_id'     => @$structure->headers['message-id'],
 173              'date'           => Date_API::getCurrentDateGMT(),
 174              'from'           => @$structure->headers['from'],
 175              'to'             => @$structure->headers['to'],
 176              'cc'             => @$structure->headers['cc'],
 177              'subject'        => @$structure->headers['subject'],
 178              'body'           => @$body,
 179              'full_email'     => @$full_message,
 180              'has_attachment' => $has_attachments,
 181              'headers'        => @$structure->headers
 182          );
 183          // automatically associate this incoming email with a customer
 184          if (Customer::hasCustomerIntegration($prj_id)) {
 185              if (!empty($structure->headers['from'])) {
 186                  list($customer_id,) = Customer::getCustomerIDByEmails($prj_id, array($sender_email));
 187                  if (!empty($customer_id)) {
 188                      $t['customer_id'] = $customer_id;
 189                  }
 190              }
 191          }
 192          if (empty($t['customer_id'])) {
 193              $t['customer_id'] = "NULL";
 194          }
 195  
 196          if (Support::blockEmailIfNeeded($t)) {
 197              return true;
 198          }
 199  
 200          // re-write Threading headers if needed
 201          list($t['full_email'], $t['headers']) = Mail_API::rewriteThreadingHeaders($t['issue_id'], $t['full_email'], $t['headers'], "email");
 202          $res = Support::insertEmail($t, $structure, $sup_id);
 203          if ($res != -1) {
 204              Support::extractAttachments($issue_id, $structure);
 205  
 206              // notifications about new emails are always external
 207              $internal_only = false;
 208              $assignee_only = false;
 209              // special case when emails are bounced back, so we don't want a notification to customers about those
 210              if (Notification::isBounceMessage($sender_email)) {
 211                  // broadcast this email only to the assignees for this issue
 212                  $internal_only = true;
 213                  $assignee_only = true;
 214              }
 215              Notification::notifyNewEmail(Auth::getUserID(), $issue_id, $t, $internal_only, $assignee_only, '', $sup_id);
 216              // try to get usr_id of sender, if not, use system account
 217              $usr_id = User::getUserIDByEmail(Mail_API::getEmailAddress($structure->headers['from']));
 218              if (!$usr_id) {
 219                  $usr_id = APP_SYSTEM_USER_ID;
 220              }
 221              // mark this issue as updated
 222              if ((!empty($t['customer_id'])) && ($t['customer_id'] != 'NULL')) {
 223                  Issue::markAsUpdated($issue_id, 'customer action');
 224              } else {
 225                  if ((!empty($usr_id)) && (User::getRoleByUser($usr_id, $prj_id) > User::getRoleID('Customer'))) {
 226                      Issue::markAsUpdated($issue_id, 'staff response');
 227                  } else {
 228                      Issue::markAsUpdated($issue_id, 'user response');
 229                  }
 230              }
 231              // log routed email
 232              History::add($issue_id, $usr_id, History::getTypeID('email_routed'), ev_gettext('Email routed from %1$s', $structure->headers['from']));
 233          }
 234  
 235          return true;
 236      }
 237  
 238  
 239      /**
 240       * Routes a note to the correct issue
 241       *
 242       * @param   string $full_message The full note
 243       * @return  mixed   true or array(ERROR_CODE, ERROR_STRING) in case of failure
 244       */
 245      function route_notes($full_message)
 246      {
 247          // save the full message for logging purposes
 248          Note::saveRoutedNote($full_message);
 249  
 250          // join the Content-Type line (for easier parsing?)
 251          if (preg_match('/^boundary=/m', $full_message)) {
 252              $pattern = "#(Content-Type: multipart/.+); ?\r?\n(boundary=.*)$#im";
 253              $replacement = '$1; $2';
 254              $full_message = preg_replace($pattern, $replacement, $full_message);
 255          }
 256  
 257          list($headers,) = Mime_Helper::splitHeaderBody($full_message);
 258  
 259          // need some validation here
 260          if (empty($full_message)) {
 261              return array(66, ev_gettext("Error: The email message was empty.") . "\n");
 262          }
 263  
 264          // remove the reply-to: header
 265          if (preg_match('/^reply-to:.*/im', $full_message)) {
 266              $full_message = preg_replace("/^(reply-to:).*\n/im", '', $full_message, 1);
 267          }
 268  
 269          // check if the email routing interface is even supposed to be enabled
 270          $setup = Setup::load();
 271          if (@$setup['note_routing']['status'] != 'enabled') {
 272              return array(78, ev_gettext("Error: The internal note routing interface is disabled.") . "\n");
 273          }
 274          if (empty($setup['note_routing']['address_prefix'])) {
 275              return array(78, ev_gettext("Error: Please configure the email address prefix.") . "\n");
 276          }
 277          if (empty($setup['note_routing']['address_host'])) {
 278              return array(78, ev_gettext("Error: Please configure the email address domain.") . "\n");
 279          }
 280          $structure = Mime_Helper::decode($full_message, true, true);
 281  
 282          // find which issue ID this email refers to
 283          if (isset($structure->headers['to'])) {
 284              $issue_id = Routing::getMatchingIssueIDs($structure->headers['to'], 'note');
 285          }
 286          // validation is always a good idea
 287          if (empty($issue_id) and isset($structure->headers['cc'])) {
 288              // we need to try the Cc header as well
 289              $issue_id = Routing::getMatchingIssueIDs($structure->headers['cc'], 'note');
 290          }
 291  
 292          if (empty($issue_id)) {
 293              return array(65, ev_gettext("Error: The routed note had no associated Eventum issue ID or had an invalid recipient address.") . "\n");
 294          }
 295  
 296          $prj_id = Issue::getProjectID($issue_id);
 297          // check if the sender is allowed in this issue' project and if it is an internal user
 298          $users = Project::getUserEmailAssocList($prj_id, 'active', User::getRoleID('Customer'));
 299          $sender_email = strtolower(Mail_API::getEmailAddress($structure->headers['from']));
 300          $user_emails = array_map('strtolower', array_values($users));
 301          if (!in_array($sender_email, $user_emails)) {
 302              return array(77, ev_gettext("Error: The sender of this email is not allowed in the project associated with issue #$issue_id.") . "\n");
 303          }
 304  
 305          Auth::createFakeCookie(User::getUserIDByEmail($sender_email), $prj_id);
 306  
 307          // parse the Cc: list, if any, and add these internal users to the issue notification list
 308          $users = array_flip($users);
 309          $addresses = array();
 310          $to_addresses = Mail_API::getEmailAddresses(@$structure->headers['to']);
 311          if (count($to_addresses)) {
 312              $addresses = $to_addresses;
 313          }
 314          $cc_addresses = Mail_API::getEmailAddresses(@$structure->headers['cc']);
 315          if (count($cc_addresses)) {
 316              $addresses = array_merge($addresses, $cc_addresses);
 317          }
 318          $cc_users = array();
 319          foreach ($addresses as $email) {
 320              if (in_array(strtolower($email), $user_emails)) {
 321                  $cc_users[] = $users[strtolower($email)];
 322              }
 323          }
 324  
 325          $body = Mime_Helper::getMessageBody($structure);
 326          $reference_msg_id = Mail_API::getReferenceMessageID($headers);
 327          if (!empty($reference_msg_id)) {
 328              $parent_id = Note::getIDByMessageID($reference_msg_id);
 329          } else {
 330              $parent_id = false;
 331          }
 332  
 333          // insert the new note and send notification about it
 334          $_POST = array(
 335              'title'                => @$structure->headers['subject'],
 336              'note'                 => $body,
 337              'note_cc'              => $cc_users,
 338              'add_extra_recipients' => 'yes',
 339              'message_id'           => @$structure->headers['message-id'],
 340              'parent_id'            => $parent_id,
 341          );
 342  
 343          // add the full email to the note if there are any attachments
 344          // this is needed because the front end code will display attachment links
 345          if (Mime_Helper::hasAttachments($structure)) {
 346              $_POST['blocked_msg'] = $full_message;
 347          }
 348          $res = Note::insert(Auth::getUserID(), $issue_id, false, false);
 349          // need to handle attachments coming from notes as well
 350          if ($res != -1) {
 351              Support::extractAttachments($issue_id, $structure, true, $res);
 352          }
 353          // FIXME! $res == -2 is not handled
 354          History::add($issue_id, Auth::getUserID(), History::getTypeID('note_routed'), ev_gettext('Note routed from %1$s', $structure->headers['from']));
 355  
 356          return true;
 357      }
 358  
 359  
 360      /**
 361       * Routes a draft to the correct issue.
 362       *
 363       * @param   string $full_message The complete draft.
 364       * @return  mixed   true or array(ERROR_CODE, ERROR_STRING) in case of failure
 365       */
 366      function route_drafts($full_message)
 367      {
 368          // save the full message for logging purposes
 369          Draft::saveRoutedMessage($full_message);
 370  
 371          if (preg_match("/^(boundary=).*/m", $full_message)) {
 372              $pattern = "/(Content-Type: multipart\/)(.+); ?\r?\n(boundary=)(.*)$/im";
 373              $replacement = '$1$2; $3$4';
 374              $full_message = preg_replace($pattern, $replacement, $full_message);
 375          }
 376  
 377          // need some validation here
 378          if (empty($full_message)) {
 379              return array(66, ev_gettext("Error: The email message was empty.") . "\n");
 380          }
 381  
 382          // remove the reply-to: header
 383          if (preg_match("/^(reply-to:).*/im", $full_message)) {
 384              $full_message = preg_replace("/^(reply-to:).*\n/im", '', $full_message, 1);
 385          }
 386  
 387          // check if the draft interface is even supposed to be enabled
 388          $setup = Setup::load();
 389          if (@$setup['draft_routing']['status'] != 'enabled') {
 390              return array(78, ev_gettext("Error: The email draft interface is disabled.") . "\n");
 391          }
 392          if (empty($setup['draft_routing']['address_prefix'])) {
 393              return array(78, ev_gettext("Error: Please configure the email address prefix.") . "\n");
 394          }
 395          if (empty($setup['draft_routing']['address_host'])) {
 396              return array(78, ev_gettext("Error: Please configure the email address domain.") . "\n");
 397          }
 398  
 399          $structure = Mime_Helper::decode($full_message, true, false);
 400  
 401          // find which issue ID this email refers to
 402          if (isset($structure->headers['to'])) {
 403              $issue_id = Routing::getMatchingIssueIDs($structure->headers['to'], 'draft');
 404          }
 405          // validation is always a good idea
 406          if (empty($issue_id) and isset($structure->headers['cc'])) {
 407              // we need to try the Cc header as well
 408              $issue_id = Routing::getMatchingIssueIDs($structure->headers['cc'], 'draft');
 409          }
 410  
 411          if (empty($issue_id)) {
 412              return array(65, ev_gettext("Error: The routed email had no associated Eventum issue ID or had an invalid recipient address.") . "\n");
 413          }
 414  
 415          $prj_id = Issue::getProjectID($issue_id);
 416          // check if the sender is allowed in this issue' project and if it is an internal user
 417          $users = Project::getUserEmailAssocList($prj_id, 'active', User::getRoleID('Customer'));
 418          $sender_email = strtolower(Mail_API::getEmailAddress($structure->headers['from']));
 419          $user_emails = array_map('strtolower', array_values($users));
 420          if (!in_array($sender_email, $user_emails)) {
 421              return array(77, ev_gettext("Error: The sender of this email is not allowed in the project associated with issue #") . "$issue_id.\n");
 422          }
 423  
 424          Auth::createFakeCookie(User::getUserIDByEmail($sender_email), $prj_id);
 425  
 426          $body = Mime_Helper::getMessageBody($structure);
 427  
 428          Draft::saveEmail($issue_id, @$structure->headers['to'], @$structure->headers['cc'], @$structure->headers['subject'], $body, false, false, false);
 429          // XXX: need to handle attachments coming from drafts as well?
 430          History::add($issue_id, Auth::getUserID(), History::getTypeID('draft_routed'), ev_gettext("Draft routed from") . " " . $structure->headers['from']);
 431          return true;
 432      }
 433  
 434      /**
 435       * Check for $adresses for matches
 436       *
 437       * @param   mixed   $addresses to check
 438       * @param   string  Type of address match to find (email, note, draft)
 439       * @return  mixed   $issue_id in case of match otherwise false
 440       */
 441      function getMatchingIssueIDs($addresses, $type)
 442      {
 443          $setup = Setup::load();
 444          $settings = $setup["$type}_routing"];
 445          if (!is_array($settings)) {
 446              return false;
 447          }
 448  
 449          if (empty($settings['address_prefix'])) {
 450              return false;
 451          }
 452          // escape plus signs so 'issue+1@example.com' becomes a valid routing address
 453          $prefix = quotemeta($settings['address_prefix']);
 454  
 455          if (empty($settings['address_host'])) {
 456              return false;
 457          }
 458          $mail_domain = quotemeta($settings['address_host']);
 459  
 460          // it is not checked for type when host alias is asked. this leaves
 461          // room foradding host_alias for other than email routing.
 462          if (isset($settings['host_alias'])) {
 463              // TODO: can't quotemeta() host alias as it can contain multiple hosts separated with pipe
 464              $mail_domain = '(?:' . $mail_domain . '|' . $settings['host_alias'] . ')';
 465          }
 466  
 467          // if there are multiple CC or To headers Mail_Mime creates array.
 468          // handle both cases (strings and arrays).
 469          if (!is_array($addresses)) {
 470              $addresses = array($addresses);
 471          }
 472  
 473          // everything safely escaped and checked, try matching address
 474          foreach ($addresses as $address) {
 475              if (preg_match("/$prefix(\d*)@$mail_domain/i", $address, $matches)) {
 476                  return $matches[1];
 477              }
 478          }
 479  
 480          return false;
 481      }
 482  }


Generated: Wed Dec 19 21:21:33 2007 Cross-referenced by PHPXref 0.7