/** * Modifies an Issue's Reporter. * * @param integer $issue_id The id of the issue. * @param string $fullname The id of the user. * @param boolean $add_history If this should be logged. * @return int */ public static function update($issue_id, $email, $add_history = true) { $email = strtolower(Mail_Helper::getEmailAddress($email)); $usr_id = User::getUserIDByEmail($email, true); // If no valid user found reset to system account if (!$usr_id) { $usr_id = APP_SYSTEM_USER_ID; } $sql = 'UPDATE {{%issue}} SET iss_usr_id = ? WHERE iss_id = ?'; try { DB_Helper::getInstance()->query($sql, array($usr_id, $issue_id)); } catch (DbException $e) { return -1; } if ($add_history) { // TRANSLATORS: %1: email, %2: full name $current_usr_id = Auth::getUserID(); History::add($issue_id, $current_usr_id, 'issue_updated', 'Reporter was changed to {email} by {user}', array('email' => $email, 'user' => User::getFullName($current_usr_id))); } // Add new user to notification list if ($usr_id > 0) { Notification::subscribeEmail($usr_id, $issue_id, $email, Notification::getDefaultActions()); } return 1; }
/** * Adds an email to the outgoing mail queue. * * @param string $recipient The recipient of this email * @param array $headers The list of headers that should be sent with this email * @param string $body The body of the message * @param integer $save_email_copy Whether to send a copy of this email to a configurable address or not (eventum_sent@) * @param integer $issue_id The ID of the issue. If false, email will not be associated with issue. * @param string $type The type of message this is. * @param integer $sender_usr_id The id of the user sending this email. * @param integer $type_id The ID of the event that triggered this notification (issue_id, sup_id, not_id, etc) * @return true, or a PEAR_Error object */ public static function add($recipient, $headers, $body, $save_email_copy = 0, $issue_id = false, $type = '', $sender_usr_id = false, $type_id = false) { Workflow::modifyMailQueue(Auth::getCurrentProject(false), $recipient, $headers, $body, $issue_id, $type, $sender_usr_id, $type_id); // avoid sending emails out to users with inactive status $recipient_email = Mail_Helper::getEmailAddress($recipient); $usr_id = User::getUserIDByEmail($recipient_email); if (!empty($usr_id)) { $user_status = User::getStatusByEmail($recipient_email); // if user is not set to an active status, then silently ignore if (!User::isActiveStatus($user_status) && !User::isPendingStatus($user_status)) { return false; } } $to_usr_id = User::getUserIDByEmail($recipient_email); $recipient = Mail_Helper::fixAddressQuoting($recipient); $reminder_addresses = Reminder::_getReminderAlertAddresses(); // add specialized headers if (!empty($issue_id) && (!empty($to_usr_id) && User::getRoleByUser($to_usr_id, Issue::getProjectID($issue_id)) != User::getRoleID('Customer')) || @in_array(Mail_Helper::getEmailAddress($recipient), $reminder_addresses)) { $headers += Mail_Helper::getSpecializedHeaders($issue_id, $type, $headers, $sender_usr_id); } // try to prevent triggering absence auto responders $headers['precedence'] = 'bulk'; // the 'classic' way, works with e.g. the unix 'vacation' tool $headers['Auto-submitted'] = 'auto-generated'; // the RFC 3834 way if (empty($issue_id)) { $issue_id = 'null'; } // if the Date: header is missing, add it. if (empty($headers['Date'])) { $headers['Date'] = Mime_Helper::encode(date('D, j M Y H:i:s O')); } if (!empty($headers['To'])) { $headers['To'] = Mail_Helper::fixAddressQuoting($headers['To']); } // encode headers and add special mime headers $headers = Mime_Helper::encodeHeaders($headers); $res = Mail_Helper::prepareHeaders($headers); if (Misc::isError($res)) { Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); return $res; } // convert array of headers into text headers list(, $text_headers) = $res; $params = array('maq_save_copy' => $save_email_copy, 'maq_queued_date' => Date_Helper::getCurrentDateGMT(), 'maq_sender_ip_address' => !empty($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '', 'maq_recipient' => $recipient, 'maq_headers' => $text_headers, 'maq_body' => $body, 'maq_iss_id' => $issue_id, 'maq_subject' => $headers['Subject'], 'maq_type' => $type); if ($sender_usr_id) { $params['maq_usr_id'] = $sender_usr_id; } if ($type_id) { $params['maq_type_id'] = $type_id; } $stmt = 'INSERT INTO {{%mail_queue}} SET ' . DB_Helper::buildSet($params); try { DB_Helper::getInstance()->query($stmt, $params); } catch (DbException $e) { return $res; } return true; }
/** * Converts a note to a draft or an email * * @param int $note_id The id of the note * @param string $target What the note should be converted too (email, etc) * @param bool $authorize_sender If $authorize_sender If the sender should be added to authorized senders list. * @return int */ public static function convertNote($note_id, $target, $authorize_sender = false) { $issue_id = self::getIssueID($note_id); $email_account_id = Email_Account::getEmailAccount(); $blocked_message = self::getBlockedMessage($note_id); $unknown_user = self::getUnknownUser($note_id); $structure = Mime_Helper::decode($blocked_message, true, true); $body = $structure->body; $sender_email = strtolower(Mail_Helper::getEmailAddress($structure->headers['from'])); $current_usr_id = Auth::getUserID(); if ($target == 'email') { if (Mime_Helper::hasAttachments($structure)) { $has_attachments = 1; } else { $has_attachments = 0; } list($blocked_message, $headers) = Mail_Helper::rewriteThreadingHeaders($issue_id, $blocked_message, @$structure->headers); $t = array('issue_id' => $issue_id, 'ema_id' => $email_account_id, 'message_id' => @$structure->headers['message-id'], 'date' => Date_Helper::getCurrentDateGMT(), 'from' => @$structure->headers['from'], 'to' => @$structure->headers['to'], 'cc' => @$structure->headers['cc'], 'subject' => @$structure->headers['subject'], 'body' => @$body, 'full_email' => @$blocked_message, 'has_attachment' => $has_attachments, 'headers' => $headers); // need to check for a possible customer association if (!empty($structure->headers['from'])) { $details = Email_Account::getDetails($email_account_id); // check from the associated project if we need to lookup any customers by this email address if (CRM::hasCustomerIntegration($details['ema_prj_id'])) { $crm = CRM::getInstance($details['ema_prj_id']); // check for any customer contact association try { $contact = $crm->getContactByEmail($sender_email); $issue_contract = $crm->getContract(Issue::getContractID($issue_id)); if ($contact->canAccessContract($issue_contract)) { $t['customer_id'] = $issue_contract->getCustomerID(); } } catch (CRMException $e) { } } } if (empty($t['customer_id'])) { $update_type = 'staff response'; $t['customer_id'] = null; } else { $update_type = 'customer action'; } $res = Support::insertEmail($t, $structure, $sup_id); if ($res != -1) { Support::extractAttachments($issue_id, $structure); // notifications about new emails are always external $internal_only = false; // special case when emails are bounced back, so we don't want to notify the customer about those if (Notification::isBounceMessage($sender_email)) { $internal_only = true; } Notification::notifyNewEmail($current_usr_id, $issue_id, $t, $internal_only, false, '', $sup_id); Issue::markAsUpdated($issue_id, $update_type); self::remove($note_id, false); History::add($issue_id, $current_usr_id, 'note_converted_email', 'Note converted to e-mail (from: {from}) by {user}', array('from' => @$structure->headers['from'], 'user' => User::getFullName($current_usr_id))); // now add sender as an authorized replier if ($authorize_sender) { Authorized_Replier::manualInsert($issue_id, @$structure->headers['from']); } } return $res; } // save message as a draft $res = Draft::saveEmail($issue_id, $structure->headers['to'], $structure->headers['cc'], $structure->headers['subject'], $body, false, $unknown_user); // remove the note, if the draft was created successfully if ($res) { self::remove($note_id, false); $usr_id = $current_usr_id; History::add($issue_id, $usr_id, 'note_converted_draft', 'Note converted to draft (from: {from}) by {user}', array('from' => @$structure->headers['from'], 'user' => User::getFullName($current_usr_id))); } return $res; }
/** * Returns if the specified user is authorized to reply to this issue. * * @param integer $issue_id The id of the issue. * @param string $email The email address to check. * @return boolean If the specified user is allowed to reply to the issue. */ public static function isAuthorizedReplier($issue_id, $email) { // XXX: Add caching $email = strtolower(Mail_Helper::getEmailAddress($email)); // first check if this is an actual user or just an email address $usr_id = User::getUserIDByEmail($email, true); if (!empty($usr_id)) { // real user, get id $is_usr_authorized = self::isUserAuthorizedReplier($issue_id, $usr_id); if ($is_usr_authorized) { return true; } // if user is not authorized by user ID, continue to check by email in case the user account was added // after the email address was added to authorized repliers list. } // not a real user $stmt = 'SELECT COUNT(*) AS total FROM {{%issue_user_replier}} WHERE iur_iss_id=? AND iur_email=?'; try { $res = DB_Helper::getInstance()->getOne($stmt, array($issue_id, $email)); } catch (DbException $e) { return false; } if ($res > 0) { return true; } else { return false; } }
$crm = CRM::getInstance($prj_id); // also need to guess the contact_id from any attached emails try { $info = $crm->getCustomerInfoFromEmails($prj_id, $item); $tpl->assign(array('customer_id' => $info['customer_id'], 'customer_name' => $info['customer_name'], 'contact_id' => $info['contact_id'], 'contact_name' => $info['contact_name'], 'contacts' => $info['contacts'])); } catch (CRMException $e) { } } // if we are dealing with just one message, use the subject line as the // summary for the issue, and the body as the description if (count($item) == 1) { $email_details = Support::getEmailDetails(Email_Account::getAccountByEmail($item[0]), $item[0]); $tpl->assign(array('issue_summary' => $email_details['sup_subject'], 'issue_description' => $email_details['seb_body'])); // also auto pre-fill the customer contact text fields if (CRM::hasCustomerIntegration($prj_id)) { $sender_email = Mail_Helper::getEmailAddress($email_details['sup_from']); try { $contact = $crm->getContactByEmail($sender_email); $tpl->assign('contact_details', $contact->getDetails()); } catch (CRMException $e) { } } } } } $tpl->assign(array('cats' => Category::getAssocList($prj_id), 'priorities' => Priority::getAssocList($prj_id), 'severities' => Severity::getList($prj_id), 'users' => Project::getUserAssocList($prj_id, 'active', User::getRoleID('Customer')), 'releases' => Release::getAssocList($prj_id), 'custom_fields' => Custom_Field::getListByProject($prj_id, 'report_form'), 'max_attachment_size' => Attachment::getMaxAttachmentSize(), 'max_attachment_bytes' => Attachment::getMaxAttachmentSize(true), 'field_display_settings' => Project::getFieldDisplaySettings($prj_id), 'groups' => Group::getAssocList($prj_id), 'products' => Product::getList(false))); $prefs = Prefs::get($usr_id); $tpl->assign('user_prefs', $prefs); $tpl->assign('zones', Date_Helper::getTimezoneList()); if (Auth::getCurrentRole() == User::getRoleID('Customer')) { $crm = CRM::getInstance(Auth::getCurrentProject());
/** * Creates an issue with the given email information. * * @param integer $prj_id The project ID * @param integer $usr_id The user responsible for this action * @param string $sender The original sender of this email * @param string $summary The issue summary * @param string $description The issue description * @param integer $category The category ID * @param integer $priority The priority ID * @param array $assignment The list of users to assign this issue to * @param string $date The date the email was originally sent. * @param string $msg_id The message ID of the email we are creating this issue from. * @param integer $severity * @param string $customer_id * @param string $contact_id * @param string $contract_id * @return int */ public static function createFromEmail($prj_id, $usr_id, $sender, $summary, $description, $category, $priority, $assignment, $date, $msg_id, $severity, $customer_id, $contact_id, $contract_id) { $exclude_list = array(); $managers = array(); $sender_email = Mail_Helper::getEmailAddress($sender); $sender_usr_id = User::getUserIDByEmail($sender_email, true); if (!empty($sender_usr_id)) { $reporter = $sender_usr_id; $exclude_list[] = $sender_usr_id; } $data = array('category' => $category, 'priority' => $priority, 'severity' => $severity, 'description' => $description, 'summary' => $summary, 'msg_id' => $msg_id, 'customer' => false, 'contact' => false, 'contract' => false, 'contact_person_lname' => '', 'contact_person_fname' => '', 'contact_email' => '', 'contact_phone' => '', 'contact_timezone' => ''); if (CRM::hasCustomerIntegration($prj_id)) { $crm = CRM::getInstance($prj_id); try { if ($contact_id != false) { $contact = $crm->getContact($contact_id); } else { $contact = $crm->getContactByEmail($sender_email); } // overwrite the reporter with the customer contact $reporter = User::getUserIDByContactID($contact->getContactID()); $data['contact'] = $contact->getContactID(); $data['contact_person_lname'] = $contact['last_name']; $data['contact_person_fname'] = $contact['first_name']; $data['contact_email'] = $sender_email; $data['contact_phone'] = $contact['phone']; $data['contact_timezone'] = Date_Helper::getPreferredTimezone($reporter); } catch (ContactNotFoundException $e) { } try { if ($contract_id != false) { $contract = $crm->getContract($contract_id); $data['contract'] = $contract->getContractID(); } elseif (isset($contact)) { // Just use first contract / customer for now. $contracts = $contact->getContracts(array('active' => true)); $contract = $contracts[0]; $data['contract'] = $contract->getContractID(); } } catch (ContractNotFoundException $e) { } try { if ($customer_id != false) { $customer = $crm->getCustomer($customer_id); $data['customer'] = $customer->getCustomerID(); } elseif (isset($contract)) { $customer = $contract->getCustomer(); $data['customer'] = $customer->getCustomerID(); } } catch (CustomerNotFoundException $e) { } } else { } if (empty($reporter)) { $reporter = APP_SYSTEM_USER_ID; } $data['reporter'] = $reporter; $issue_id = self::insertIssue($prj_id, $data); if ($issue_id == -1) { return -1; } $has_RR = false; // log the creation of the issue History::add($issue_id, $usr_id, 'issue_opened', 'Issue opened by {sender}', array('sender' => $sender)); $emails = array(); // if there are any technical account managers associated with this customer, add these users to the notification list if ($data['customer']) { $managers = CRM::getAccountManagers($prj_id, $data['customer']); foreach ($managers as $manager) { $emails[] = $manager['usr_email']; } } // add the reporter to the notification list $emails[] = $sender; $emails = array_unique($emails); $actions = Notification::getDefaultActions($issue_id, false, 'issue_from_email'); foreach ($emails as $address) { Notification::subscribeEmail($reporter, $issue_id, $address, $actions); } // only assign the issue to an user if the associated customer has any technical account managers $users = array(); $has_TAM = false; if (CRM::hasCustomerIntegration($prj_id) && count($managers) > 0) { foreach ($managers as $manager) { if ($manager['cam_type'] == 'intpart') { continue; } $users[] = $manager['cam_usr_id']; self::addUserAssociation($usr_id, $issue_id, $manager['cam_usr_id'], false); History::add($issue_id, $usr_id, 'issue_auto_assigned', 'Issue auto-assigned to {assignee} (TAM)', array('assignee' => User::getFullName($manager['cam_usr_id']))); } $has_TAM = true; } // now add the user/issue association if (@count($assignment) > 0) { foreach ($assignment as $ass_usr_id) { Notification::subscribeUser($reporter, $issue_id, $ass_usr_id, $actions); self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $ass_usr_id); if ($ass_usr_id != $usr_id) { $users[] = $ass_usr_id; } } } else { // only use the round-robin feature if this new issue was not // already assigned to a customer account manager if (count($managers) < 1) { $assignee = Round_Robin::getNextAssignee($prj_id); // assign the issue to the round robin person if (!empty($assignee)) { self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $assignee, false); History::add($issue_id, APP_SYSTEM_USER_ID, 'rr_issue_assigned', 'Issue auto-assigned to {assignee} (RR)', array('assignee' => User::getFullName($assignee))); $users[] = $assignee; $has_RR = true; } } } Workflow::handleNewIssue($prj_id, $issue_id, $has_TAM, $has_RR); // send special 'an issue was auto-created for you' notification back to the sender Notification::notifyAutoCreatedIssue($prj_id, $issue_id, $sender, $date, $summary); // also notify any users that want to receive emails anytime a new issue is created Notification::notifyNewIssue($prj_id, $issue_id, $exclude_list); return $issue_id; }
/** * Check if this email needs to be blocked and if so, block it. * * */ public static function blockEmailIfNeeded($email) { if (empty($email['issue_id'])) { return false; } $issue_id = $email['issue_id']; $prj_id = Issue::getProjectID($issue_id); $sender_email = strtolower(Mail_Helper::getEmailAddress($email['from'])); list($text_headers, $body) = Mime_Helper::splitHeaderBody($email['full_email']); if (Mail_Helper::isVacationAutoResponder($email['headers']) || Notification::isBounceMessage($sender_email) || !self::isAllowedToEmail($issue_id, $sender_email)) { // add the message body as a note $_POST = array('full_message' => $email['full_email'], 'title' => @$email['headers']['subject'], 'note' => Mail_Helper::getCannedBlockedMsgExplanation($issue_id) . $email['body'], 'message_id' => Mail_Helper::getMessageID($text_headers, $body)); // avoid having this type of message re-open the issue if (Mail_Helper::isVacationAutoResponder($email['headers'])) { $closing = true; $notify = false; } else { $closing = false; $notify = true; } $res = Note::insertFromPost(Auth::getUserID(), $issue_id, $email['headers']['from'], false, $closing, $notify, true); // associate the email attachments as internal-only files on this issue if ($res != -1) { self::extractAttachments($issue_id, $email['full_email'], true, $res); } $_POST['issue_id'] = $issue_id; $_POST['from'] = $sender_email; // avoid having this type of message re-open the issue if (Mail_Helper::isVacationAutoResponder($email['headers'])) { $email_type = 'vacation-autoresponder'; } else { $email_type = 'routed'; } Workflow::handleBlockedEmail($prj_id, $issue_id, $_POST, $email_type); // try to get usr_id of sender, if not, use system account $usr_id = User::getUserIDByEmail(Mail_Helper::getEmailAddress($email['from']), true); if (!$usr_id) { $usr_id = APP_SYSTEM_USER_ID; } History::add($issue_id, $usr_id, 'email_blocked', "Email from '{from}' blocked", array('from' => $email['from'])); return true; } return false; }
/** * Method used to get a list of emails that are associated with a given * project and issue. * * @param integer $prj_id The project ID * @param integer $issue_id The issue ID * @return array List of emails */ public static function getAddressBookEmails($prj_id, $issue_id) { $list = self::getAddressBook($prj_id, $issue_id); $emails = array(); foreach ($list as $address => $name) { $emails[] = Mail_Helper::getEmailAddress($address); } return $emails; }
} } $tpl->assign('associate_result', $res); } @$tpl->assign('total_emails', count($_POST['item'])); } else { @$tpl->assign('emails', $_GET['item']); @$tpl->assign('total_emails', count($_GET['item'])); $prj_id = Issue::getProjectID($_GET['issue_id']); if (CRM::hasCustomerIntegration($prj_id)) { // check if the selected emails all have sender email addresses that are associated with the issue' customer $crm = CRM::getInstance($prj_id); $senders = Support::getSender($_GET['item']); $sender_emails = array(); foreach ($senders as $sender) { $email = Mail_Helper::getEmailAddress($sender); $sender_emails[$email] = $sender; } $contract_id = Issue::getContractID($_GET['issue_id']); if (!empty($contract_id)) { try { $contract = $crm->getContract($contract_id); // TODOCRM: Active contacts only $contact_emails = array_keys($contract->getContactEmailAssocList()); } catch (CRMException $e) { $contact_emails = array(); } $unknown_contacts = array(); foreach ($sender_emails as $email => $address) { if (!@in_array($email, $contact_emails)) { $usr_id = User::getUserIDByEmail($email);
/** * Method used to update the details of a given subscription. * * @param $issue_id * @param integer $sub_id The subscription ID * @param $email * @return integer 1 if the update worked, -1 otherwise */ public static function update($issue_id, $sub_id, $email) { $usr_id = User::getUserIDByEmail(strtolower(Mail_Helper::getEmailAddress($email)), true); if (!empty($usr_id)) { $email = ''; } else { $usr_id = 0; } $prj_id = Issue::getProjectID($issue_id); // call workflow to modify actions or cancel adding this user. $actions = array(); $subscriber_usr_id = false; $workflow = Workflow::handleSubscription($prj_id, $issue_id, $subscriber_usr_id, $email, $actions); if ($workflow === false) { // cancel subscribing the user return -2; } // always set the type of notification to issue-level $stmt = "UPDATE\n {{%subscription}}\n SET\n sub_level='issue',\n sub_email=?,\n sub_usr_id=?\n WHERE\n sub_id=?"; try { DB_Helper::getInstance()->query($stmt, array($email, $usr_id, $sub_id)); } catch (DbException $e) { return -1; } $stmt = 'DELETE FROM {{%subscription_type}} WHERE sbt_sub_id=?'; DB_Helper::getInstance()->query($stmt, array($sub_id)); // now add them all again foreach ($_POST['actions'] as $sbt_type) { // FIXME: $sbt_type not validated for sane values self::addType($sub_id, $sbt_type); } // need to mark the issue as updated Issue::markAsUpdated($issue_id); $current_usr_id = Auth::getUserID(); History::add($issue_id, $current_usr_id, 'notification_updated', "Notification list entry ('{subscriber}') updated by {user}", array('subscriber' => self::getSubscriber($sub_id), 'user' => User::getFullName($current_usr_id))); return 1; }
/** * Routes a draft to the correct issue. * * @param string $full_message The complete draft. * @return mixed true or array(ERROR_CODE, ERROR_STRING) in case of failure */ public static function route_drafts($full_message) { // save the full message for logging purposes Draft::saveRoutedMessage($full_message); if (preg_match('/^(boundary=).*/m', $full_message)) { $pattern = "/(Content-Type: multipart\\/)(.+); ?\r?\n(boundary=)(.*)\$/im"; $replacement = '$1$2; $3$4'; $full_message = preg_replace($pattern, $replacement, $full_message); } // need some validation here if (empty($full_message)) { return array(self::EX_NOINPUT, ev_gettext('Error: The email message was empty.') . "\n"); } // remove the reply-to: header if (preg_match('/^(reply-to:).*/im', $full_message)) { $full_message = preg_replace("/^(reply-to:).*\n/im", '', $full_message, 1); } // check if the draft interface is even supposed to be enabled $setup = Setup::get(); if ($setup['draft_routing']['status'] != 'enabled') { return array(self::EX_CONFIG, ev_gettext('Error: The email draft interface is disabled.') . "\n"); } if (empty($setup['draft_routing']['address_prefix'])) { return array(self::EX_CONFIG, ev_gettext('Error: Please configure the email address prefix.') . "\n"); } if (empty($setup['draft_routing']['address_host'])) { return array(self::EX_CONFIG, ev_gettext('Error: Please configure the email address domain.') . "\n"); } $structure = Mime_Helper::decode($full_message, true, false); // find which issue ID this email refers to if (isset($structure->headers['to'])) { $issue_id = self::getMatchingIssueIDs($structure->headers['to'], 'draft'); } // validation is always a good idea if (empty($issue_id) and isset($structure->headers['cc'])) { // we need to try the Cc header as well $issue_id = self::getMatchingIssueIDs($structure->headers['cc'], 'draft'); } if (empty($issue_id)) { return array(self::EX_DATAERR, ev_gettext('Error: The routed email had no associated Eventum issue ID or had an invalid recipient address.') . "\n"); } $prj_id = Issue::getProjectID($issue_id); // check if the sender is allowed in this issue' project and if it is an internal user $sender_email = strtolower(Mail_Helper::getEmailAddress($structure->headers['from'])); $sender_usr_id = User::getUserIDByEmail($sender_email, true); if (!empty($sender_usr_id)) { $sender_role = User::getRoleByUser($sender_usr_id, $prj_id); if ($sender_role < User::ROLE_USER) { return array(self::EX_NOPERM, ev_gettext("Error: The sender of this email is not allowed in the project associated with issue #{$issue_id}.") . "\n"); } } AuthCookie::setAuthCookie(User::getUserIDByEmail($sender_email)); AuthCookie::setProjectCookie($prj_id); $body = $structure->body; Draft::saveEmail($issue_id, @$structure->headers['to'], @$structure->headers['cc'], @$structure->headers['subject'], $body, false, false, false); // XXX: need to handle attachments coming from drafts as well? $usr_id = Auth::getUserID(); History::add($issue_id, $usr_id, 'draft_routed', 'Draft routed from {from}', array('from' => $structure->headers['from'])); return true; }