function updatePropertiesAction() { @($id = DevblocksPlatform::importGPC($_REQUEST['id'])); // ticket id @($closed = DevblocksPlatform::importGPC($_REQUEST['closed'], 'integer', 0)); @($spam = DevblocksPlatform::importGPC($_REQUEST['spam'], 'integer', 0)); @($deleted = DevblocksPlatform::importGPC($_REQUEST['deleted'], 'integer', 0)); @($bucket = DevblocksPlatform::importGPC($_REQUEST['bucket_id'], 'string')); @($next_worker_id = DevblocksPlatform::importGPC($_REQUEST['next_worker_id'], 'integer', 0)); @($unlock_date = DevblocksPlatform::importGPC($_REQUEST['unlock_date'], 'integer', 0)); @($ticket = DAO_Ticket::getTicket($id)); // Anti-Spam if (!empty($spam)) { CerberusBayes::markTicketAsSpam($id); // [mdf] if the spam button was clicked override the default params for deleted/closed $closed = 1; $deleted = 1; } $categories = DAO_Bucket::getAll(); // Properties $properties = array(DAO_Ticket::IS_CLOSED => intval($closed), DAO_Ticket::IS_DELETED => intval($deleted)); // Undeleting? if (empty($spam) && empty($closed) && empty($deleted) && $ticket->spam_training == CerberusTicketSpamTraining::SPAM && $ticket->is_closed) { $score = CerberusBayes::calculateTicketSpamProbability($id); $properties[DAO_Ticket::SPAM_SCORE] = $score['probability']; $properties[DAO_Ticket::SPAM_TRAINING] = CerberusTicketSpamTraining::BLANK; } // Team/Category if (!empty($bucket)) { list($team_id, $bucket_id) = CerberusApplication::translateTeamCategoryCode($bucket); if (!empty($team_id)) { $properties[DAO_Ticket::TEAM_ID] = $team_id; $properties[DAO_Ticket::CATEGORY_ID] = $bucket_id; } } if ($next_worker_id != $ticket->next_worker_id) { $properties[DAO_Ticket::NEXT_WORKER_ID] = $next_worker_id; } // Reset the unlock date (next worker "until") $properties[DAO_Ticket::UNLOCK_DATE] = $unlock_date; DAO_Ticket::updateTicket($id, $properties); DevblocksPlatform::setHttpResponse(new DevblocksHttpResponse(array('display', $id))); }
function viewSpamTicketsAction() { @($view_id = DevblocksPlatform::importGPC($_REQUEST['view_id'], 'string')); @($ticket_ids = DevblocksPlatform::importGPC($_REQUEST['ticket_id'], 'array')); $fields = array(DAO_Ticket::IS_CLOSED => 1, DAO_Ticket::IS_DELETED => 1); //==================================== // Undo functionality $last_action = new Model_TicketViewLastAction(); $last_action->action = Model_TicketViewLastAction::ACTION_SPAM; if (is_array($ticket_ids)) { foreach ($ticket_ids as $ticket_id) { // CerberusBayes::calculateTicketSpamProbability($ticket_id); // [TODO] Ugly (optimize -- use the 'interesting_words' to do a word bayes spam score? $last_action->ticket_ids[$ticket_id] = array(DAO_Ticket::SPAM_TRAINING => CerberusTicketSpamTraining::BLANK, DAO_Ticket::SPAM_SCORE => 0.5, DAO_Ticket::IS_CLOSED => 0, DAO_Ticket::IS_DELETED => 0); } } $last_action->action_params = $fields; C4_TicketView::setLastAction($view_id, $last_action); //==================================== // {TODO] Batch if (!empty($ticket_ids)) { foreach ($ticket_ids as $id) { CerberusBayes::markTicketAsSpam($id); } } DAO_Ticket::updateTicket($ticket_ids, $fields); $view = C4_AbstractViewLoader::getView($view_id); $view->render(); return; }
/** * @param integer[] $ticket_ids */ function run($ticket_ids) { $fields = array(); $field_values = array(); $groups = DAO_Group::getAll(); $buckets = DAO_Bucket::getAll(); $workers = DAO_Worker::getAll(); $custom_fields = DAO_CustomField::getAll(); // actions if (is_array($this->actions)) { foreach ($this->actions as $action => $params) { switch ($action) { case 'status': if (isset($params['is_waiting'])) { $fields[DAO_Ticket::IS_WAITING] = intval($params['is_waiting']); } if (isset($params['is_closed'])) { $fields[DAO_Ticket::IS_CLOSED] = intval($params['is_closed']); } if (isset($params['is_deleted'])) { $fields[DAO_Ticket::IS_DELETED] = intval($params['is_deleted']); } break; case 'assign': if (isset($params['worker_id'])) { $w_id = intval($params['worker_id']); if (0 == $w_id || isset($workers[$w_id])) { $fields[DAO_Ticket::NEXT_WORKER_ID] = $w_id; } } break; case 'move': if (isset($params['group_id']) && isset($params['bucket_id'])) { $g_id = intval($params['group_id']); $b_id = intval($params['bucket_id']); if (isset($groups[$g_id]) && (0 == $b_id || isset($buckets[$b_id]))) { $fields[DAO_Ticket::TEAM_ID] = $g_id; $fields[DAO_Ticket::CATEGORY_ID] = $b_id; } } break; case 'spam': if (isset($params['is_spam'])) { if (intval($params['is_spam'])) { foreach ($ticket_ids as $ticket_id) { CerberusBayes::markTicketAsSpam($ticket_id); } } else { foreach ($ticket_ids as $ticket_id) { CerberusBayes::markTicketAsNotSpam($ticket_id); } } } break; default: // Custom fields if (substr($action, 0, 3) == "cf_") { $field_id = intval(substr($action, 3)); if (!isset($custom_fields[$field_id]) || !isset($params['value'])) { break; } $field_values[$field_id] = $params; } break; } } } if (!empty($ticket_ids)) { if (!empty($fields)) { DAO_Ticket::updateTicket($ticket_ids, $fields); } // Custom Fields C4_AbstractView::_doBulkSetCustomFields(ChCustomFieldSource_Ticket::ID, $field_values, $ticket_ids); } }
/** * Enter description here... * * @param CerberusParserMessage $message * @return integer */ public static function parseMessage(CerberusParserMessage $message, $options = array()) { // print_r($rfcMessage); /* * options: * 'no_autoreply' */ $logger = DevblocksPlatform::getConsoleLog(); $settings = CerberusSettings::getInstance(); $helpdesk_senders = CerberusApplication::getHelpdeskSenders(); $headers =& $message->headers; // To/From/Cc/Bcc $sReturnPath = @$headers['return-path']; $sReplyTo = @$headers['reply-to']; $sFrom = @$headers['from']; $sTo = @$headers['to']; $bIsNew = true; // Overloadable $sMask = ''; $iClosed = 0; $enumSpamTraining = ''; $iDate = time(); $from = array(); $to = array(); if (!empty($sReplyTo)) { $from = CerberusParser::parseRfcAddress($sReplyTo); } elseif (!empty($sFrom)) { $from = CerberusParser::parseRfcAddress($sFrom); } elseif (!empty($sReturnPath)) { $from = CerberusParser::parseRfcAddress($sReturnPath); } if (!empty($sTo)) { // [TODO] Do we still need this RFC address parser? $to = CerberusParser::parseRfcAddress($sTo); } // Subject // Fix quote printable subject (quoted blocks can appear anywhere in subject) $sSubject = ""; if (isset($headers['subject']) && !empty($headers['subject'])) { $sSubject = self::fixQuotePrintableString($headers['subject']); } // The subject can still end up empty after QP decode if (empty($sSubject)) { $sSubject = "(no subject)"; } // Date $iDate = @strtotime($headers['date']); // If blank, or in the future, set to the current date if (empty($iDate) || $iDate > time()) { $iDate = time(); } if (empty($from) || !is_array($from)) { $logger->warn("[Parser] Invalid 'From' address: " . $from); return NULL; } @($fromAddress = $from[0]->mailbox . '@' . $from[0]->host); @($fromPersonal = $from[0]->personal); if (null == ($fromAddressInst = CerberusApplication::hashLookupAddress($fromAddress, true))) { $logger->err("[Parser] 'From' address could not be created: " . $fromAddress); return NULL; } else { $fromAddressId = $fromAddressInst->id; } // Is banned? if (1 == $fromAddressInst->is_banned) { $logger->info("[Parser] Ignoring ticket from banned address: " . $fromAddressInst->email); return NULL; } // Message Id / References / In-Reply-To @($sMessageId = $headers['message-id']); $body_append_text = array(); $body_append_html = array(); // [mdf]Check attached files before creating the ticket because we may need to overwrite the message-id // also store any contents of rfc822 files so we can include them after the body foreach ($message->files as $filename => $file) { /* @var $file ParserFile */ switch ($file->mime_type) { case 'message/rfc822': $full_filename = $file->tmpname; $mail = mailparse_msg_parse_file($full_filename); $struct = mailparse_msg_get_structure($mail); $msginfo = mailparse_msg_get_part_data($mail); $inline_headers = $msginfo['headers']; if (isset($headers['from']) && (strtolower(substr($headers['from'], 0, 11)) == 'postmaster@' || strtolower(substr($headers['from'], 0, 14)) == 'mailer-daemon@')) { $headers['in-reply-to'] = $inline_headers['message-id']; } break; } } // [JAS] [TODO] References header may contain multiple message-ids to find if (null != ($ids = self::findParentMessage($headers))) { $bIsNew = false; $id = $ids['ticket_id']; $msgid = $ids['message_id']; // Is it a worker reply from an external client? If so, proxy if (null != ($worker_address = DAO_AddressToWorker::getByAddress($fromAddressInst->email))) { $logger->info("[Parser] Handling an external worker response from " . $fromAddressInst->email); if (!DAO_Ticket::isTicketRequester($worker_address->address, $id)) { // Watcher Commands [TODO] Document on wiki/etc if (0 != ($matches = preg_match_all("/\\[(.*?)\\]/i", $message->headers['subject'], $commands))) { @($command = strtolower(array_pop($commands[1]))); $logger->info("[Parser] Worker command: " . $command); switch ($command) { case 'close': DAO_Ticket::updateTicket($id, array(DAO_Ticket::IS_CLOSED => CerberusTicketStatus::CLOSED)); break; case 'take': DAO_Ticket::updateTicket($id, array(DAO_Ticket::NEXT_WORKER_ID => $worker_address->worker_id)); break; case 'comment': $comment_id = DAO_TicketComment::create(array(DAO_TicketComment::ADDRESS_ID => $fromAddressId, DAO_TicketComment::CREATED => time(), DAO_TicketComment::TICKET_ID => $id, DAO_TicketComment::COMMENT => $message->body)); return $id; break; default: // Typo? break; } } $attachment_files = array(); $attachment_files['name'] = array(); $attachment_files['type'] = array(); $attachment_files['tmp_name'] = array(); $attachment_files['size'] = array(); $i = 0; foreach ($message->files as $filename => $file) { $attachment_files['name'][$i] = $filename; $attachment_files['type'][$i] = $file->mime_type; $attachment_files['tmp_name'][$i] = $file->tmpname; $attachment_files['size'][$i] = $file->file_size; $i++; } CerberusMail::sendTicketMessage(array('message_id' => $msgid, 'content' => $message->body, 'files' => $attachment_files, 'agent_id' => $worker_address->worker_id)); return $id; } else { // ... worker is a requester, treat as normal $logger->info("[Parser] The external worker was a ticket requester, so we're not treating them as a watcher."); } } else { // Reply: Not sent by a worker /* * [TODO] check that this sender is a requester on the matched ticket * Otherwise blank out the $id */ } } @(list($team_id, $matchingToAddress) = CerberusParser::findDestination($headers)); // Pre-parse mail rules if (null != ($pre_filter = self::_checkPreParseRules(empty($id) ? 1 : 0, $fromAddress, $team_id, $message))) { // Do something with matching filter's actions foreach ($pre_filter->actions as $action_key => $action) { switch ($action_key) { case 'blackhole': return NULL; break; case 'redirect': @($to = $action['to']); CerberusMail::reflect($message, $to); return NULL; break; case 'bounce': @($msg = $action['message']); // [TODO] Follow the RFC spec on a true bounce CerberusMail::quickSend($fromAddress, "Delivery failed: " . $sSubject, $msg); return NULL; break; } } } if (empty($id)) { // New Ticket // Are we delivering or bouncing? if (empty($team_id)) { // Bounce return null; } if (empty($sMask)) { $sMask = CerberusApplication::generateTicketMask(); } $fields = array(DAO_Ticket::MASK => $sMask, DAO_Ticket::SUBJECT => $sSubject, DAO_Ticket::IS_CLOSED => $iClosed, DAO_Ticket::FIRST_WROTE_ID => intval($fromAddressId), DAO_Ticket::LAST_WROTE_ID => intval($fromAddressId), DAO_Ticket::CREATED_DATE => $iDate, DAO_Ticket::UPDATED_DATE => $iDate, DAO_Ticket::TEAM_ID => intval($team_id), DAO_Ticket::LAST_ACTION_CODE => CerberusTicketActionCode::TICKET_OPENED); $id = DAO_Ticket::createTicket($fields); } // [JAS]: Add requesters to the ticket if (!empty($fromAddressId) && !empty($id)) { // Don't add a requester if the sender is a helpdesk address if (isset($helpdesk_senders[$fromAddressInst->email])) { $logger->info("[Parser] Not adding ourselves as a requester: " . $fromAddressInst->email); } else { DAO_Ticket::createRequester($fromAddressId, $id); } } // Add the other TO/CC addresses to the ticket // [TODO] This should be cleaned up and optimized if ($settings->get(CerberusSettings::PARSER_AUTO_REQ, 0)) { @($autoreq_exclude_list = $settings->get(CerberusSettings::PARSER_AUTO_REQ_EXCLUDE, '')); $destinations = self::getDestinations($headers); if (is_array($destinations) && !empty($destinations)) { // Filter out any excluded requesters if (!empty($autoreq_exclude_list)) { @($autoreq_exclude = DevblocksPlatform::parseCrlfString($autoreq_exclude_list)); if (is_array($autoreq_exclude) && !empty($autoreq_exclude)) { foreach ($autoreq_exclude as $excl_pattern) { $excl_regexp = DevblocksPlatform::parseStringAsRegExp($excl_pattern); // Check all destinations for this pattern foreach ($destinations as $idx => $dest) { if (@preg_match($excl_regexp, $dest)) { unset($destinations[$idx]); } } } } } foreach ($destinations as $dest) { if (null != ($destInst = CerberusApplication::hashLookupAddress($dest, true))) { // Skip if the destination is one of our senders or the matching TO if (isset($helpdesk_senders[$destInst->email]) || 0 == strcasecmp($matchingToAddress, $destInst->email)) { continue; } DAO_Ticket::createRequester($destInst->id, $id); } } } } $attachment_path = APP_STORAGE_PATH . '/attachments/'; // [TODO] This should allow external attachments (S3) $fields = array(DAO_Message::TICKET_ID => $id, DAO_Message::CREATED_DATE => $iDate, DAO_Message::ADDRESS_ID => $fromAddressId); $email_id = DAO_Message::create($fields); // Content DAO_MessageContent::create($email_id, $message->body); // Headers foreach ($headers as $hk => $hv) { DAO_MessageHeader::create($email_id, $id, $hk, $hv); } // [mdf] Loop through files to insert attachment records in the db, and move temporary files if (!empty($email_id)) { foreach ($message->files as $filename => $file) { /* @var $file ParserFile */ //[mdf] skip rfc822 messages since we extracted their content above if ($file->mime_type == 'message/rfc822') { continue; } $fields = array(DAO_Attachment::MESSAGE_ID => $email_id, DAO_Attachment::DISPLAY_NAME => $filename, DAO_Attachment::MIME_TYPE => $file->mime_type, DAO_Attachment::FILE_SIZE => intval($file->file_size)); $file_id = DAO_Attachment::create($fields); if (empty($file_id)) { @unlink($file->tmpname); // remove our temp file continue; } // Make file attachments use buckets so we have a max per directory $attachment_bucket = sprintf("%03d/", mt_rand(1, 100)); $attachment_file = $file_id; if (!file_exists($attachment_path . $attachment_bucket)) { @mkdir($attachment_path . $attachment_bucket, 0770, true); // [TODO] Needs error checking } rename($file->getTempFile(), $attachment_path . $attachment_bucket . $attachment_file); // [TODO] Split off attachments into its own DAO DAO_Attachment::update($file_id, array(DAO_Attachment::FILEPATH => $attachment_bucket . $attachment_file)); } } // First Thread if ($bIsNew && !empty($email_id)) { // First thread DAO_Ticket::updateTicket($id, array(DAO_Ticket::FIRST_MESSAGE_ID => $email_id)); } // New ticket processing if ($bIsNew) { // Don't replace this with the master event listener if (false !== ($rules = CerberusApplication::runGroupRouting($team_id, $id))) { /* @var $rule Model_GroupInboxFilter */ // Check the last match which moved the ticket if (is_array($rules)) { foreach ($rules as $rule) { // If a rule changed our destination, replace the scope variable $team_id if (isset($rule->actions['move']) && isset($rule->actions['move']['group_id'])) { $team_id = intval($rule->actions['move']['group_id']); } } } } // Allow spam training overloading if (!empty($enumSpamTraining)) { if ($enumSpamTraining == CerberusTicketSpamTraining::SPAM) { CerberusBayes::markTicketAsSpam($id); DAO_Ticket::updateTicket($id, array(DAO_Ticket::IS_CLOSED => 1, DAO_Ticket::IS_DELETED => 1)); } elseif ($enumSpamTraining == CerberusTicketSpamTraining::NOT_SPAM) { CerberusBayes::markTicketAsNotSpam($id); } } else { // No overload $out = CerberusBayes::calculateTicketSpamProbability($id); // [TODO] Move this group logic to a post-parse event listener if (!empty($team_id)) { @($spam_threshold = DAO_GroupSettings::get($team_id, DAO_GroupSettings::SETTING_SPAM_THRESHOLD, 80)); @($spam_action = DAO_GroupSettings::get($team_id, DAO_GroupSettings::SETTING_SPAM_ACTION, '')); @($spam_action_param = DAO_GroupSettings::get($team_id, DAO_GroupSettings::SETTING_SPAM_ACTION_PARAM, '')); if ($out['probability'] * 100 >= $spam_threshold) { $enumSpamTraining = CerberusTicketSpamTraining::SPAM; switch ($spam_action) { default: case 0: // do nothing break; case 1: // delete // [TODO] Would have been much nicer to delete before this point DAO_Ticket::updateTicket($id, array(DAO_Ticket::IS_CLOSED => 1, DAO_Ticket::IS_DELETED => 1)); break; case 2: // move $buckets = DAO_Bucket::getAll(); // Verify bucket exists if (!empty($spam_action_param) && isset($buckets[$spam_action_param])) { DAO_Ticket::updateTicket($id, array(DAO_Ticket::TEAM_ID => $team_id, DAO_Ticket::CATEGORY_ID => $spam_action_param)); } break; } } } } // end spam training // Auto reply @($autoreply_enabled = DAO_GroupSettings::get($team_id, DAO_GroupSettings::SETTING_AUTO_REPLY_ENABLED, 0)); @($autoreply = DAO_GroupSettings::get($team_id, DAO_GroupSettings::SETTING_AUTO_REPLY, '')); /* * Send the group's autoreply if one exists, as long as this ticket isn't spam */ if (!isset($options['no_autoreply']) && $autoreply_enabled && !empty($autoreply) && $enumSpamTraining != CerberusTicketSpamTraining::SPAM) { CerberusMail::sendTicketMessage(array('ticket_id' => $id, 'message_id' => $email_id, 'content' => str_replace(array('#ticket_id#', '#mask#', '#subject#', '#timestamp#', '#sender#', '#sender_first#', '#orig_body#'), array($id, $sMask, $sSubject, date('r'), $fromAddress, $fromAddressInst->first_name, ltrim($message->body)), $autoreply), 'is_autoreply' => true, 'dont_keep_copy' => true)); } } // end bIsNew unset($message); // Re-open and update our date on new replies if (!$bIsNew) { DAO_Ticket::updateTicket($id, array(DAO_Ticket::UPDATED_DATE => time(), DAO_Ticket::IS_WAITING => 0, DAO_Ticket::IS_CLOSED => 0, DAO_Ticket::IS_DELETED => 0, DAO_Ticket::LAST_WROTE_ID => $fromAddressId, DAO_Ticket::LAST_ACTION_CODE => CerberusTicketActionCode::TICKET_CUSTOMER_REPLY)); // [TODO] The TICKET_CUSTOMER_REPLY should be sure of this message address not being a worker } // Inbound Reply Event $eventMgr = DevblocksPlatform::getEventService(); $eventMgr->trigger(new Model_DevblocksEvent('ticket.reply.inbound', array('ticket_id' => $id, 'message_id' => $email_id))); @imap_errors(); // Prevent errors from spilling out into STDOUT return $id; }