Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge branch '6.1.1' into stable

  • Loading branch information...
commit 0a55954ae53cbd454f838f60a46acb0263ef822d 2 parents c68c4c3 + 2135b67
@jstanden jstanden authored
Showing with 478 additions and 163 deletions.
  1. +4 −2 api/Application.class.php
  2. +8 −0 api/app/Mail.php
  3. +171 −118 api/app/Parser.php
  4. +14 −1 features/cerberusweb.core/api/dao/message.php
  5. +17 −1 features/cerberusweb.core/api/dao/ticket.php
  6. +9 −2 features/cerberusweb.core/api/events/app/mail_received_by_app.php
  7. +2 −2 features/cerberusweb.core/api/uri/config/mail_incoming.php
  8. +53 −0 features/cerberusweb.core/api/uri/config/mail_relay.php
  9. +1 −8 features/cerberusweb.core/api/uri/display.php
  10. +2 −2 features/cerberusweb.core/api/uri/internal.php
  11. +5 −1 features/cerberusweb.core/api/uri/internal/dashboards.php
  12. +13 −0 features/cerberusweb.core/plugin.xml
  13. +1 −0  features/cerberusweb.core/templates/configuration/index.tpl
  14. +64 −0 features/cerberusweb.core/templates/configuration/section/mail_relay/index.tpl
  15. +25 −0 features/cerberusweb.core/templates/internal/views/criteria/__time_elapsed.tpl
  16. +1 −1  features/cerberusweb.core/templates/internal/workspaces/widgets/chart/scatterplot.tpl
  17. +2 −0  features/cerberusweb.core/templates/mail/section/compose/peek.tpl
  18. +7 −0 features/cerberusweb.crm/api/dao/crm_opportunity.php
  19. +79 −25 libs/devblocks/api/Model.php
View
6 api/Application.class.php
@@ -46,8 +46,8 @@
* - Jeff Standen, Darren Sugita, Dan Hildebrandt, Scott Luther
* WEBGROUP MEDIA LLC. - Developers of Cerberus Helpdesk
*/
-define("APP_BUILD", 2012082801);
-define("APP_VERSION", '6.1.0');
+define("APP_BUILD", 2012090401);
+define("APP_VERSION", '6.1.1');
define("APP_MAIL_PATH", APP_STORAGE_PATH . '/mail/');
@@ -1410,6 +1410,7 @@ class CerberusSettings {
const TICKET_MASK_FORMAT = 'ticket_mask_format';
const AUTHORIZED_IPS = 'authorized_ips';
const LICENSE = 'license_json';
+ const RELAY_DISABLE_AUTH = 'relay_disable_auth';
};
class CerberusSettingsDefaults {
@@ -1428,6 +1429,7 @@ class CerberusSettingsDefaults {
const PARSER_AUTO_REQ_EXCLUDE = '';
const TICKET_MASK_FORMAT = 'LLL-NNNNN-NNN';
const AUTHORIZED_IPS = "127.0.0.1\n::1\n";
+ const RELAY_DISABLE_AUTH = 0;
};
class C4_DevblocksExtensionDelegate implements DevblocksExtensionDelegate {
View
8 api/app/Mail.php
@@ -819,6 +819,14 @@ static function sendTicketMessage($properties=array()) {
}
}
}
+
+ // Forwarded attachments
+ if(isset($properties['link_forward_files']) && !empty($properties['link_forward_files'])) {
+ // Attachments
+ if(is_array($forward_files) && !empty($forward_files)) {
+ DAO_AttachmentLink::setLinks(CerberusContexts::CONTEXT_MESSAGE, $message_id, $forward_files);
+ }
+ }
}
if(isset($properties['owner_id'])) {
View
289 api/app/Parser.php
@@ -224,7 +224,20 @@ private function _parseHeadersIsNew() {
foreach(array_keys($aReferences) as $ref) {
if(empty($ref))
continue;
+
+ if(preg_match('#\<(.*)\_(\d*)\_(\d*)\_([a-f0-9]{8})\@cerb5\>#', $ref, $hits)) {
+ $ticket_id = $hits[2];
+ if(null != ($ticket = DAO_Ticket::get($ticket_id))) {
+ $this->_is_new = false;
+ $this->_ticket_id = $ticket_id;
+ $this->_ticket_model = $ticket;
+ $this->_message_id = $ticket->last_message_id;
+ return;
+ }
+ }
+
+ // Otherwise, look up the normal header
if(null != ($ids = DAO_Ticket::getTicketByMessageId($ref))) {
$this->_is_new = false;
$this->_ticket_id = $ids['ticket_id'];
@@ -769,126 +782,166 @@ static public function parseMessage(CerberusParserMessage $message, $options=arr
// Overloadable
$enumSpamTraining = '';
- // Is it a worker reply from an external client? If so, proxy
- if(null != ($worker_address = DAO_AddressToWorker::getByAddress($model->getSenderAddressModel()->email))) {
- $logger->info("[Worker Relay] Handling an external worker relay for " . $model->getSenderAddressModel()->email);
-
- @$in_reply_to = $message->headers['in-reply-to'];
- @$subject = $message->headers['subject'];
- $proxy_worker = DAO_Worker::get($worker_address->worker_id);
-
- // If it's a watcher reply
- if(preg_match('#\<(.*)\_(\d*)\_(\d*)\_([a-f0-9]{8})\@cerb5\>#', $in_reply_to, $hits)) {
- $context = $hits[1];
- $context_id = $hits[2];
- $signed = $hits[4];
-
- $signed_compare = substr(md5($context.$context_id.$proxy_worker->pass),8,8);
+ // Is it a worker reply from an external client? If so, proxy
+
+ if(null != ($proxy_ticket = $model->getTicketModel())
+ && null != ($worker_address = DAO_AddressToWorker::getByAddress($model->getSenderAddressModel()->email))) {
+
+ $logger->info("[Worker Relay] Handling an external worker relay for " . $model->getSenderAddressModel()->email);
- // Compare worker signature, then auth
- if($signed_compare == $signed) {
- $logger->info("[Worker Relay] Worker authentication successful. Proceeding.");
-
- CerberusContexts::setActivityDefaultActor(CerberusContexts::CONTEXT_WORKER, $proxy_worker->id);
-
- // [TODO] Expand contexts
- // [TODO] Plugify contexts
- switch($context) {
- case CerberusContexts::CONTEXT_TICKET:
- if(null != ($ticket = DAO_Ticket::get($context_id))) {
- // Properties
- $properties = array(
- 'ticket_id' => $ticket->id,
- 'message_id' => $ticket->last_message_id,
- //'files' => $attachment_files,
- 'worker_id' => $proxy_worker->id,
- );
-
- // Clean the reply body
- $body = '';
- $lines = DevblocksPlatform::parseCrlfString($message->body, true);
- $is_cut = false;
-
- foreach($lines as $line) {
- if(preg_match('/[\s\>]*\s*##/', $line))
- continue;
+ $proxy_worker = DAO_Worker::get($worker_address->worker_id);
+ $is_authenticated = false;
+
+ // If it's a watcher reply
+ $relay_auth_disabled = DevblocksPlatform::getPluginSetting('cerberusweb.core', CerberusSettings::RELAY_DISABLE_AUTH, CerberusSettingsDefaults::RELAY_DISABLE_AUTH);
- // Insert worker sig for this bucket
- if(preg_match('/^#sig/', $line, $matches)) {
- $group = DAO_Group::get($ticket->group_id);
- $sig = $group->getReplySignature($ticket->bucket_id, $proxy_worker);
- $body .= $sig . PHP_EOL;
-
- } elseif(preg_match('/^#cut/', $line, $matches)) {
- $is_cut = true;
-
- } elseif(preg_match('/^#watch/', $line, $matches)) {
- CerberusContexts::addWatchers($context, $context_id, $proxy_worker->id);
-
- } elseif(preg_match('/^#unwatch/', $line, $matches)) {
- CerberusContexts::removeWatchers($context, $context_id, $proxy_worker->id);
-
- } elseif(preg_match('/^#noreply/', $line, $matches)) {
- $properties['dont_send'] = 1;
- $properties['dont_keep_copy'] = 1;
-
- } elseif(preg_match('/^#status (.*)/', $line, $matches)) {
- switch(strtolower($matches[1])) {
- case 'o':
- case 'open':
- $properties['closed'] = 0;
- break;
- case 'w':
- case 'waiting':
- $properties['closed'] = 2;
- break;
- case 'c':
- case 'closed':
- $properties['closed'] = 1;
- break;
- }
-
- } elseif(preg_match('/^#reopen (.*)/', $line, $matches)) {
- $properties['ticket_reopen'] = $matches[1];
-
- } elseif(preg_match('/^#comment (.*)/', $line, $matches)) {
- if(!isset($matches[1]) || empty($matches[1]))
- continue;
-
- DAO_Comment::create(array(
- DAO_Comment::CREATED => time(),
- DAO_Comment::ADDRESS_ID => $model->getSenderAddressModel()->id,
- DAO_Comment::COMMENT => $matches[1],
- DAO_Comment::CONTEXT => $context,
- DAO_Comment::CONTEXT_ID => $context_id,
- ));
-
- } else {
- if(!$is_cut)
- $body .= $line . PHP_EOL;
- }
- }
-
- $properties['content'] = $body;
-
- $result = CerberusMail::sendTicketMessage($properties);
- return NULL;
- }
- break;
- }
-
- // Clear temporary worker session
- CerberusContexts::setActivityDefaultActor(null);
-
- } else { // failed worker auth
- // [TODO] Bounce
- $logger->error("[Worker Relay] Worker authentication failed. Ignoring.");
- return false;
- }
-
- }
-
- }
+ if($relay_auth_disabled) {
+ $is_authenticated = true;
+
+ } else {
+ @$in_reply_to = $message->headers['in-reply-to'];
+
+ if(preg_match('#\<(.*)\_(\d*)\_(\d*)\_([a-f0-9]{8})\@cerb5\>#', $in_reply_to, $hits)) {
+ $proxy_context = $hits[1];
+ $proxy_context_id = $hits[2];
+ $signed = $hits[4];
+
+ $signed_compare = substr(md5($proxy_context.$proxy_context_id.$proxy_worker->pass),8,8);
+
+ $is_authenticated = ($signed_compare == $signed);
+
+ unset($hits);
+ unset($proxy_context);
+ unset($proxy_context_id);
+ unset($signed);
+ unset($signed_compare);
+ unset($in_reply_to);
+ }
+ }
+
+ // Compare worker signature, then auth
+ if($is_authenticated) {
+ $logger->info("[Worker Relay] Worker authentication successful. Proceeding.");
+
+ CerberusContexts::setActivityDefaultActor(CerberusContexts::CONTEXT_WORKER, $proxy_worker->id);
+
+ if(!empty($proxy_ticket)) {
+ $parser_message = $model->getMessage();
+ $attachment_file_ids = array();
+
+ foreach($parser_message->files as $filename => $file) {
+ if(0 == strcasecmp($filename, 'original_message.html'))
+ continue;
+
+ $fields = array(
+ DAO_Attachment::DISPLAY_NAME => $filename,
+ DAO_Attachment::MIME_TYPE => $file->mime_type,
+ );
+
+ if(null == ($file_id = DAO_Attachment::create($fields))) {
+ @unlink($file->tmpname); // remove our temp file
+ continue;
+ }
+
+ $attachment_file_ids[] = $file_id;
+
+ if(null !== ($fp = fopen($file->getTempFile(), 'rb'))) {
+ Storage_Attachments::put($file_id, $fp);
+ fclose($fp);
+ unlink($file->getTempFile());
+ }
+ }
+
+ // Properties
+ $properties = array(
+ 'ticket_id' => $proxy_ticket->id,
+ 'message_id' => $proxy_ticket->last_message_id,
+ 'forward_files' => $attachment_file_ids,
+ 'link_forward_files' => true,
+ 'worker_id' => $proxy_worker->id,
+ );
+
+ // Clean the reply body
+ $body = '';
+ $lines = DevblocksPlatform::parseCrlfString($message->body, true);
+ $is_cut = false;
+
+ foreach($lines as $line) {
+ if(preg_match('/[\s\>]*\s*##/', $line))
+ continue;
+
+ // Insert worker sig for this bucket
+ if(preg_match('/^#sig/', $line, $matches)) {
+ $group = DAO_Group::get($proxy_ticket->group_id);
+ $sig = $group->getReplySignature($proxy_ticket->bucket_id, $proxy_worker);
+ $body .= $sig . PHP_EOL;
+
+ } elseif(preg_match('/^#cut/', $line, $matches)) {
+ $is_cut = true;
+
+ } elseif(preg_match('/^#watch/', $line, $matches)) {
+ CerberusContexts::addWatchers(CerberusContexts::CONTEXT_TICKET, $proxy_ticket->id, $proxy_worker->id);
+
+ } elseif(preg_match('/^#unwatch/', $line, $matches)) {
+ CerberusContexts::removeWatchers(CerberusContexts::CONTEXT_TICKET, $proxy_ticket->id, $proxy_worker->id);
+
+ } elseif(preg_match('/^#noreply/', $line, $matches)) {
+ $properties['dont_send'] = 1;
+ $properties['dont_keep_copy'] = 1;
+
+ } elseif(preg_match('/^#status (.*)/', $line, $matches)) {
+ switch(strtolower($matches[1])) {
+ case 'o':
+ case 'open':
+ $properties['closed'] = 0;
+ break;
+ case 'w':
+ case 'waiting':
+ $properties['closed'] = 2;
+ break;
+ case 'c':
+ case 'closed':
+ $properties['closed'] = 1;
+ break;
+ }
+
+ } elseif(preg_match('/^#reopen (.*)/', $line, $matches)) {
+ $properties['ticket_reopen'] = $matches[1];
+
+ } elseif(preg_match('/^#comment (.*)/', $line, $matches)) {
+ if(!isset($matches[1]) || empty($matches[1]))
+ continue;
+
+ DAO_Comment::create(array(
+ DAO_Comment::CREATED => time(),
+ DAO_Comment::ADDRESS_ID => $model->getSenderAddressModel()->id,
+ DAO_Comment::COMMENT => $matches[1],
+ DAO_Comment::CONTEXT => CerberusContexts::CONTEXT_TICKET,
+ DAO_Comment::CONTEXT_ID => $proxy_ticket->id,
+ ));
+
+ } else {
+ if(!$is_cut)
+ $body .= $line . PHP_EOL;
+ }
+ }
+
+ $properties['content'] = $body;
+
+ $result = CerberusMail::sendTicketMessage($properties);
+ return NULL;
+ }
+
+ // Clear temporary worker session
+ CerberusContexts::setActivityDefaultActor(null);
+
+ } else { // failed worker auth
+ // [TODO] Bounce
+ $logger->error("[Worker Relay] Worker authentication failed. Ignoring.");
+ return false;
+ }
+
+ }
// New Ticket
if($model->getIsNew()) {
View
15 features/cerberusweb.core/api/dao/message.php
@@ -1218,10 +1218,14 @@ function renderCriteria($field) {
$tpl->display('devblocks:cerberusweb.core::internal/views/criteria/__string.tpl');
break;
- case SearchFields_Message::RESPONSE_TIME:
+ case '_placeholder_number':
$tpl->display('devblocks:cerberusweb.core::internal/views/criteria/__number.tpl');
break;
+ case SearchFields_Message::RESPONSE_TIME:
+ $tpl->display('devblocks:cerberusweb.core::internal/views/criteria/__time_elapsed.tpl');
+ break;
+
case SearchFields_Message::IS_BROADCAST:
case SearchFields_Message::IS_OUTGOING:
case SearchFields_Message::TICKET_IS_DELETED:
@@ -1283,6 +1287,11 @@ function renderCriteriaParam($param) {
$this->_renderCriteriaParamWorker($param);
break;
+ case SearchFields_Message::RESPONSE_TIME:
+ $value = array_shift($values);
+ echo DevblocksPlatform::strSecsToString($value);
+ break;
+
default:
parent::renderCriteriaParam($param);
break;
@@ -1304,6 +1313,10 @@ function doSetCriteria($field, $oper, $value) {
break;
case SearchFields_Message::RESPONSE_TIME:
+ $now = time();
+ @$then = intval(strtotime($value, $now));
+ $value = $then - $now;
+
$criteria = new DevblocksSearchCriteria($field,$oper,$value);
break;
View
18 features/cerberusweb.core/api/dao/ticket.php
@@ -2217,9 +2217,12 @@ function renderCriteria($field) {
case SearchFields_Ticket::TICKET_FIRST_WROTE_NONSPAM:
case SearchFields_Ticket::TICKET_ID:
case SearchFields_Ticket::TICKET_NUM_MESSAGES:
+ $tpl->display('devblocks:cerberusweb.core::internal/views/criteria/__number.tpl');
+ break;
+
case SearchFields_Ticket::TICKET_ELAPSED_RESPONSE_FIRST:
case SearchFields_Ticket::TICKET_ELAPSED_RESOLUTION_FIRST:
- $tpl->display('devblocks:cerberusweb.core::internal/views/criteria/__number.tpl');
+ $tpl->display('devblocks:cerberusweb.core::internal/views/criteria/__time_elapsed.tpl');
break;
case SearchFields_Ticket::TICKET_WAITING:
@@ -2487,6 +2490,12 @@ function renderCriteriaParam($param) {
echo implode(", ", $strings);
break;
+ case SearchFields_Ticket::TICKET_ELAPSED_RESOLUTION_FIRST:
+ case SearchFields_Ticket::TICKET_ELAPSED_RESPONSE_FIRST:
+ $value = array_shift($values);
+ echo DevblocksPlatform::strSecsToString($value);
+ break;
+
default:
parent::renderCriteriaParam($param);
break;
@@ -2523,8 +2532,15 @@ function doSetCriteria($field, $oper, $value) {
case SearchFields_Ticket::TICKET_FIRST_WROTE_NONSPAM:
case SearchFields_Ticket::TICKET_ID:
case SearchFields_Ticket::TICKET_NUM_MESSAGES:
+ $criteria = new DevblocksSearchCriteria($field,$oper,$value);
+ break;
+
case SearchFields_Ticket::TICKET_ELAPSED_RESPONSE_FIRST:
case SearchFields_Ticket::TICKET_ELAPSED_RESOLUTION_FIRST:
+ $now = time();
+ @$then = intval(strtotime($value, $now));
+ $value = $then - $now;
+
$criteria = new DevblocksSearchCriteria($field,$oper,$value);
break;
View
11 features/cerberusweb.core/api/events/app/mail_received_by_app.php
@@ -330,18 +330,22 @@ function runConditionExtension($token, $trigger, $params, DevblocksDictionaryDel
$oper = ltrim($params['oper'],'!');
@$header = strtolower($params['header']);
@$param_value = $params['value'];
-
+
if(!isset($dict->headers[$header])) {
$value = '';
} else {
$value = $dict->headers[$header];
}
+ if(is_array($value))
+ $value = implode("\n", $value);
+
// Operators
switch($oper) {
case 'is':
- $pass = (0==strcasecmp($value,$param_value));
+ $pass = (0==strcasecmp($value, $param_value));
break;
+
case 'like':
if(isset($dict->headers[$header])) {
$regexp = DevblocksPlatform::strToRegExp($param_value);
@@ -350,12 +354,15 @@ function runConditionExtension($token, $trigger, $params, DevblocksDictionaryDel
$pass = false;
}
break;
+
case 'contains':
$pass = (false !== stripos($value, $param_value)) ? true : false;
break;
+
case 'regexp':
$pass = @preg_match($param_value, $value);
break;
+
default:
$pass = false;
break;
View
4 features/cerberusweb.core/api/uri/config/mail_incoming.php
@@ -82,9 +82,9 @@ function testMaskAction() {
));
$sample_mask = CerberusApplication::generateTicketMask($ticket_mask_format);
- $output = sprintf("<b>%s</b> &nbsp; There are %0.0f possible ticket mask combinations.",
+ $output = sprintf("<b>%s</b> &nbsp; There are %s possible ticket mask combinations.",
$sample_mask,
- $cardinality
+ number_format($cardinality, 0)
);
echo json_encode(array('status'=>true,'message'=>$output));
View
53 features/cerberusweb.core/api/uri/config/mail_relay.php
@@ -0,0 +1,53 @@
+<?php
+/***********************************************************************
+| Cerb(tm) developed by WebGroup Media, LLC.
+|-----------------------------------------------------------------------
+| All source code & content (c) Copyright 2012, WebGroup Media LLC
+| unless specifically noted otherwise.
+|
+| This source code is released under the Devblocks Public License.
+| The latest version of this license can be found here:
+| http://cerberusweb.com/license
+|
+| By using this software, you acknowledge having read this license
+| and agree to be bound thereby.
+| ______________________________________________________________________
+| http://www.cerberusweb.com http://www.webgroupmedia.com/
+***********************************************************************/
+
+class PageSection_SetupMailRelay extends Extension_PageSection {
+ function render() {
+ $tpl = DevblocksPlatform::getTemplateService();
+ $visit = CerberusApplication::getVisit();
+
+ $visit->set(ChConfigurationPage::ID, 'mail_relay');
+
+ $tpl->display('devblocks:cerberusweb.core::configuration/section/mail_relay/index.tpl');
+ }
+
+ function saveJsonAction() {
+ try {
+ $translate = DevblocksPlatform::getTranslationService();
+ $worker = CerberusApplication::getActiveWorker();
+
+ if(!$worker || !$worker->is_superuser)
+ throw new Exception("You are not an administrator.");
+
+ @$relay_disable_auth = DevblocksPlatform::importGPC($_POST['relay_disable_auth'],'integer',0);
+
+ // Save
+
+ $settings = DevblocksPlatform::getPluginSettingsService();
+ $settings->set('cerberusweb.core',CerberusSettings::RELAY_DISABLE_AUTH, $relay_disable_auth);
+
+ echo json_encode(array('status'=>true));
+ return;
+
+ } catch (Exception $e) {
+ echo json_encode(array('status'=>false,'error'=>$e->getMessage()));
+ return;
+
+ }
+
+ }
+}
View
9 features/cerberusweb.core/api/uri/display.php
@@ -520,6 +520,7 @@ function sendReplyAction() {
'ticket_reopen' => DevblocksPlatform::importGPC(@$_REQUEST['ticket_reopen'],'string',''),
'worker_id' => @$worker->id,
'forward_files' => $file_ids,
+ 'link_forward_files' => true,
);
// Custom fields
@@ -537,14 +538,6 @@ function sendReplyAction() {
if(CerberusMail::sendTicketMessage($properties)) {
if(!empty($draft_id))
DAO_MailQueue::delete($draft_id);
-
- // Attachments
- // [TODO] This could happen inside ::sendTicketMessage()
- if(is_array($file_ids) && !empty($file_ids)) {
- if(null != ($ticket = DAO_Ticket::get($ticket_id))) {
- DAO_AttachmentLink::setLinks(CerberusContexts::CONTEXT_MESSAGE, $ticket->last_message_id, $file_ids);
- }
- }
}
// Automatically add new 'To:' recipients?
View
4 features/cerberusweb.core/api/uri/internal.php
@@ -2810,8 +2810,8 @@ function showScheduleBehaviorBulkParamsAction() {
$tpl = DevblocksPlatform::getTemplateService();
- $trigger = DAO_TriggerEvent::get($trigger_id);
- $tpl->assign('macro_params', $trigger->variables);
+ if(null != ($trigger = DAO_TriggerEvent::get($trigger_id)))
+ $tpl->assign('macro_params', $trigger->variables);
$tpl->display('devblocks:cerberusweb.core::internal/macros/behavior/bulk_params.tpl');
}
View
6 features/cerberusweb.core/api/uri/internal/dashboards.php
@@ -1096,9 +1096,13 @@ function render(Model_WorkspaceWidget $widget) {
// echo $sql,"<br>\n";
+ $counter = 0;
+
foreach($results as $result) {
+ $x = ($series['xaxis_field'] == '_id') ? $counter++ : (float)$result['xaxis'];
+
$data[] = array(
- 'x' => (float)$result['xaxis'],
+ 'x' => $x,
'y' => (float)$result['yaxis'],
'x_label' => (float)$result['xaxis'],
'y_label' => (float)$result['yaxis'],
View
13 features/cerberusweb.core/plugin.xml
@@ -1837,6 +1837,19 @@
</extension>
<extension point="cerberusweb.ui.page.section">
+ <id>core.page.setup.mail_relay</id>
+ <name>Mail Relay Section</name>
+ <class>
+ <file>api/uri/config/mail_relay.php</file>
+ <name>PageSection_SetupMailRelay</name>
+ </class>
+ <params>
+ <param key="page_id" value="core.page.configuration" />
+ <param key="uri" value="mail_relay" />
+ </params>
+ </extension>
+
+ <extension point="cerberusweb.ui.page.section">
<id>core.page.setup.mail_from</id>
<name>Mail From Section</name>
<class>
View
1  features/cerberusweb.core/templates/configuration/index.tpl
@@ -47,6 +47,7 @@
<li><a href="{devblocks_url}c=config&a=mail_pop3{/devblocks_url}">POP3 Accounts</a></li>
<li><a href="{devblocks_url}c=config&a=mail_routing{/devblocks_url}">Routing</a></li>
<li><a href="{devblocks_url}c=config&a=mail_filtering{/devblocks_url}">Filtering</a></li>
+ <li><a href="{devblocks_url}c=config&a=mail_relay{/devblocks_url}">External Relay</a></li>
<li><hr></li>
<li><b>Outgoing Mail</b></li>
View
64 features/cerberusweb.core/templates/configuration/section/mail_relay/index.tpl
@@ -0,0 +1,64 @@
+<h2>External Mail Relay</h2>
+
+<form id="frmSetupMailRelay" action="{devblocks_url}{/devblocks_url}" method="post" onsubmit="return false;">
+<input type="hidden" name="c" value="config">
+<input type="hidden" name="a" value="handleSectionAction">
+<input type="hidden" name="section" value="mail_relay">
+<input type="hidden" name="action" value="saveJson">
+
+<p>
+The email relay enables workers to respond to messages from external mail applications (e.g. Gmail, mobile phones, Outlook, etc) instead of always requiring them to use Cerb in the web browser.
+Relayed responses are received from a worker's personal email address and rewritten so they appear to be from Cerb before being sent to a conversation's recipients.
+This process protects the privacy of personal worker email addresses, while still providing the benefits of Cerb (e.g. shared history, assignments, etc).
+</p>
+
+<fieldset>
+ <legend>Authentication</legend>
+ <p>
+ By default, relayed messages are authenticated by checking the mail headers.
+ Copies of mail that are relayed to workers outside of Cerb using Virtual Attendant behavior are "signed" with a secret key in the <tt>Message-Id:</tt> header.
+ According to the RFC-2822 standard, this <tt>Message-Id:</tt> should be referenced in the <tt>In-Reply-To:</tt> header of any reply.
+ </p>
+
+ <p>
+ Unfortunately, some email applications "break the Internet" by ignoring these many decade old conventions.
+ Common culprits include Microsoft Exchange and some Android and Blackberry mobile devices.
+ </p>
+
+ <p>
+ In the event that the worker relay doesn't function properly in your environment, you may disable the built-in authentication.
+ <b>Be careful when doing this!</b>
+ When authentication is disabled, anyone can forge a message <tt>From:</tt> one of your workers and have it relayed to arbitrary conversations.
+ It is very important that you set up alternative authentication using Virtual Attendants in Mail Filtering to approve or deny inbound worker replies through the relay.
+ </p>
+
+ <p>
+ See the <a href="http://cerbweb.com/book/latest/cookbook" target="_blank">cookbook</a> for more information.
+ </p>
+ <br>
+
+ <b>Built-in relay authentication:</b>
+ {$relay_disable_auth = $settings->get('cerberusweb.core','relay_disable_auth',CerberusSettingsDefaults::RELAY_DISABLE_AUTH)}
+ <p>
+ <label><input type="radio" name="relay_disable_auth" value="0" {if empty($relay_disable_auth)}checked="checked"{/if}> {'common.enabled'|devblocks_translate|capitalize}</label>
+ <label><input type="radio" name="relay_disable_auth" value="1" {if $relay_disable_auth}checked="checked"{/if}> {'common.disabled'|devblocks_translate|capitalize}</label>
+ </p>
+</fieldset>
+
+<div class="status"></div>
+
+</form>
+
+<script type="text/javascript">
+ $('#frmSetupMailRelay INPUT:radio')
+ .click(function(e) {
+ genericAjaxPost('frmSetupMailRelay','',null,function(json) {
+ $o = $.parseJSON(json);
+ if(false == $o || false == $o.status) {
+ Devblocks.showError('#frmSetupMailRelay div.status',$o.error);
+ }
+ });
+ })
+ ;
+ ;
+</script>
View
25 features/cerberusweb.core/templates/internal/views/criteria/__time_elapsed.tpl
@@ -0,0 +1,25 @@
+<b>{$translate->_('search.operator')|capitalize}:</b><br>
+<blockquote style="margin:5px;">
+ <select name="oper">
+ <option value="=" {if $param && $param->operator=='='}selected="selected"{/if}>{$translate->_('search.oper.equals')}</option>
+ <option value="!=" {if $param && $param->operator=='!='}selected="selected"{/if}>{$translate->_('search.oper.equals.not')}</option>
+ <option value="&gt;" {if $param && $param->operator=='>'}selected="selected"{/if}>&gt;</option>
+ <option value="&lt;" {if $param && $param->operator=='<'}selected="selected"{/if}>&lt;</option>
+ </select>
+</blockquote>
+
+<b>{$translate->_('search.value')|capitalize}:</b><br>
+<blockquote style="margin:5px;">
+ <input type="text" name="value" value="{$param->value|devblocks_prettysecs}"><br>
+</blockquote>
+
+<div style="margin-top:10px;">
+ Examples:
+ <ul style="margin:0px 0px 10px 0px;">
+ <li>5 seconds</li>
+ <li>15 mins</li>
+ <li>2 hours</li>
+ <li>1 day, 3 hours</li>
+ <li>1 week, 2 days, 3 hours</li>
+ </ul>
+</div>
View
2  features/cerberusweb.core/templates/internal/workspaces/widgets/chart/scatterplot.tpl
@@ -177,7 +177,7 @@ try {
context.arc(closest.chart_x, closest.chart_y, 5, 0, 2 * Math.PI, false);
context.fill();
- text = closest.data.x + ', ' + closest.data.y;
+ text = closest.data.x_label + ', ' + closest.data.y_label;
bounds = context.measureText(text);
padding = 2;
View
2  features/cerberusweb.core/templates/mail/section/compose/peek.tpl
@@ -134,6 +134,8 @@
{/if}
{/foreach}
</select>
+ <button type="button" onclick="$(this).prev('select[name=owner_id]').val('{$active_worker->id}');">{'common.me'|devblocks_translate|lower}</button>
+ <button type="button" onclick="$(this).prevAll('select[name=owner_id]').first().val('');">{'common.nobody'|devblocks_translate|lower}</button>
</td>
</tr>
<tr>
View
7 features/cerberusweb.crm/api/dao/crm_opportunity.php
@@ -467,6 +467,7 @@ class SearchFields_CrmOpportunity implements IDevblocksSearchFields {
const EMAIL_ADDRESS = 'a_email';
const EMAIL_FIRST_NAME = 'a_first_name';
+ const EMAIL_IS_DEFUNCT = 'a_is_defunct';
const EMAIL_LAST_NAME = 'a_last_name';
const EMAIL_NUM_SPAM = 'a_num_spam';
const EMAIL_NUM_NONSPAM = 'a_num_nonspam';
@@ -493,6 +494,7 @@ static function getFields() {
self::PRIMARY_EMAIL_ID => new DevblocksSearchField(self::PRIMARY_EMAIL_ID, 'o', 'primary_email_id', $translate->_('crm.opportunity.primary_email_id')),
self::EMAIL_ADDRESS => new DevblocksSearchField(self::EMAIL_ADDRESS, 'a', 'email', $translate->_('crm.opportunity.email_address'), Model_CustomField::TYPE_SINGLE_LINE),
+ self::EMAIL_IS_DEFUNCT => new DevblocksSearchField(self::EMAIL_IS_DEFUNCT, 'a', 'is_defunct', $translate->_('address.is_defunct'), Model_CustomField::TYPE_CHECKBOX),
self::EMAIL_FIRST_NAME => new DevblocksSearchField(self::EMAIL_FIRST_NAME, 'a', 'first_name', $translate->_('address.first_name'), Model_CustomField::TYPE_SINGLE_LINE),
self::EMAIL_LAST_NAME => new DevblocksSearchField(self::EMAIL_LAST_NAME, 'a', 'last_name', $translate->_('address.last_name'), Model_CustomField::TYPE_SINGLE_LINE),
self::EMAIL_NUM_SPAM => new DevblocksSearchField(self::EMAIL_NUM_SPAM, 'a', 'num_spam', $translate->_('address.num_spam'), Model_CustomField::TYPE_NUMBER),
@@ -628,6 +630,7 @@ function getSubtotalFields() {
switch($field_key) {
// Strings
case SearchFields_CrmOpportunity::EMAIL_ADDRESS:
+ case SearchFields_CrmOpportunity::EMAIL_IS_DEFUNCT:
case SearchFields_CrmOpportunity::EMAIL_FIRST_NAME:
case SearchFields_CrmOpportunity::EMAIL_LAST_NAME:
case SearchFields_CrmOpportunity::IS_CLOSED:
@@ -671,6 +674,7 @@ function getSubtotalCounts($column) {
break;
case SearchFields_CrmOpportunity::IS_CLOSED:
+ case SearchFields_CrmOpportunity::EMAIL_IS_DEFUNCT:
case SearchFields_CrmOpportunity::IS_WON:
$counts = $this->_getSubtotalCountForBooleanColumn('DAO_CrmOpportunity', $column);
break;
@@ -755,6 +759,7 @@ function renderCriteria($field) {
$tpl->display('devblocks:cerberusweb.core::internal/views/criteria/__number.tpl');
break;
+ case SearchFields_CrmOpportunity::EMAIL_IS_DEFUNCT:
case SearchFields_CrmOpportunity::IS_CLOSED:
case SearchFields_CrmOpportunity::IS_WON:
$tpl->display('devblocks:cerberusweb.core::internal/views/criteria/__bool.tpl');
@@ -796,6 +801,7 @@ function renderCriteriaParam($param) {
$values = !is_array($param->value) ? array($param->value) : $param->value;
switch($field) {
+ case SearchFields_CrmOpportunity::EMAIL_IS_DEFUNCT:
case SearchFields_CrmOpportunity::IS_CLOSED:
case SearchFields_CrmOpportunity::IS_WON:
$this->_renderCriteriaParamBoolean($param);
@@ -829,6 +835,7 @@ function doSetCriteria($field, $oper, $value) {
$criteria = new DevblocksSearchCriteria($field,$oper,$value);
break;
+ case SearchFields_CrmOpportunity::EMAIL_IS_DEFUNCT:
case SearchFields_CrmOpportunity::IS_CLOSED:
case SearchFields_CrmOpportunity::IS_WON:
@$bool = DevblocksPlatform::importGPC($_REQUEST['bool'],'integer',1);
View
104 libs/devblocks/api/Model.php
@@ -99,38 +99,65 @@ public function getWhereSQL($fields) {
break;
case "in":
- if(!is_array($this->value)) break;
- $value = (!empty($this->value)) ? $this->value : array(-1);
+ if(!is_array($this->value) && !is_string($this->value))
+ break;
+
+ if(!is_array($this->value) && preg_match('#^\[.*\]$#', $this->value)) {
+ $values = json_decode($this->value, true);
+
+ } elseif(is_array($this->value)) {
+ $values = $this->value;
+
+ } else {
+ $values = array($this->value);
+
+ }
+
$vals = array();
// Escape quotes
- foreach($this->value as $idx=>$val) {
- $vals[$idx] = addslashes($val);
+ foreach($values as $idx=>$val) {
+ $vals[$idx] = $db->qstr((string)$val);
}
- $where = sprintf("%s IN ('%s')",
+ if(empty($vals))
+ $vals = array(-1);
+
+ $where = sprintf("%s IN (%s)",
$db_field_name,
- implode("','",$vals)
+ implode(",", $vals)
);
break;
case DevblocksSearchCriteria::OPER_IN_OR_NULL:
- if(!is_array($this->value)) break;
- $value = (!empty($this->value)) ? $this->value : array(-1);
+ if(!is_array($this->value) && !is_string($this->value))
+ break;
+
+ if(!is_array($this->value) && preg_match('#^\[.*\]$#', $this->value)) {
+ $values = json_decode($this->value, true);
+
+ } elseif(is_array($this->value)) {
+ $values = $this->value;
+
+ } else {
+ $values = array($this->value);
+
+ }
+
$vals = array();
// Escape quotes
- foreach($this->value as $idx=>$val) {
- $vals[$idx] = addslashes($val);
+ foreach($values as $idx=>$val) {
+ $vals[$idx] = $db->qstr((string)$val);
}
if(empty($vals)) {
$where_in = '';
- } else {
- $where_in = sprintf("%s IN ('%s') OR ",
+ } elseif(is_string($this->value)) {
+ $where_in = sprintf("%s IN (%s) OR ",
$db_field_name,
- implode("','",$vals)
+ implode(",",$vals)
);
}
@@ -141,38 +168,65 @@ public function getWhereSQL($fields) {
break;
case DevblocksSearchCriteria::OPER_NIN: // 'not in'
- if(!is_array($this->value)) break;
- $value = (!empty($this->value)) ? $this->value : array(-1);
+ if(!is_array($this->value) && !is_string($this->value))
+ break;
+
+ if(!is_array($this->value) && preg_match('#^\[.*\]$#', $this->value)) {
+ $values = json_decode($this->value, true);
+
+ } elseif(is_array($this->value)) {
+ $values = $this->value;
+
+ } else {
+ $values = array($this->value);
+
+ }
+
$vals = array();
// Escape quotes
- foreach($this->value as $idx=>$val) {
- $vals[$idx] = addslashes($val);
+ foreach($values as $idx=>$val) {
+ $vals[$idx] = $db->qstr((string)$val);
}
+
+ if(empty($vals))
+ $vals = array(-1);
- $where = sprintf("%s NOT IN ('%s')",
+ $where = sprintf("%s NOT IN (%s)",
$db_field_name,
- implode("','",$vals)
+ implode(",",$vals)
);
break;
case DevblocksSearchCriteria::OPER_NIN_OR_NULL:
- if(!is_array($this->value)) break;
- $value = (!empty($this->value)) ? $this->value : array(-1);
+ if(!is_array($this->value) && !is_string($this->value))
+ break;
+
+ if(!is_array($this->value) && preg_match('#^\[.*\]$#', $this->value)) {
+ $values = json_decode($this->value, true);
+
+ } elseif(is_array($this->value)) {
+ $values = $this->value;
+
+ } else {
+ $values = array($this->value);
+
+ }
+
$vals = array();
// Escape quotes
- foreach($this->value as $idx=>$val) {
- $vals[$idx] = addslashes($val);
+ foreach($values as $idx=>$val) {
+ $vals[$idx] = $db->qstr((string)$val);
}
if(empty($vals)) {
$where_in = '';
} else {
- $where_in = sprintf("%s NOT IN ('%s') OR ",
+ $where_in = sprintf("%s NOT IN (%s) OR ",
$db_field_name,
- implode("','",$vals)
+ implode(",",$vals)
);
}
Please sign in to comment.
Something went wrong with that request. Please try again.