[ 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: 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 }
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 |