From 4742bc178c819c1c962c8d9429140217b54aafdc Mon Sep 17 00:00:00 2001 From: aydreeihn <adriane@enhancesoft.com> Date: Tue, 19 Sep 2017 16:15:13 -0500 Subject: [PATCH] new branch with bcc/cc and security updates --- assets/default/css/theme.css | 22 ++ include/ajax.content.php | 1 + include/ajax.thread.php | 6 +- include/class.client.php | 24 +- include/class.collaborator.php | 84 +++- include/class.email.php | 4 +- include/class.mailer.php | 15 +- include/class.task.php | 8 + include/class.thread.php | 139 ++++++- include/class.thread_actions.php | 67 +++- include/class.ticket.php | 362 ++++++++++++++---- include/class.user.php | 16 + .../client/templates/thread-entry.tmpl.php | 10 +- .../client/templates/thread-event.tmpl.php | 2 +- include/client/tickets.inc.php | 7 +- include/client/view.inc.php | 27 +- .../en_US/templates/email/message.alert.yaml | 2 +- .../staff/templates/collaborators.tmpl.php | 75 ++-- include/staff/templates/task-view.tmpl.php | 1 - .../thread-email-recipients.tmpl.php | 30 ++ include/staff/templates/thread-entry.tmpl.php | 16 +- include/staff/ticket-open.inc.php | 104 ++++- include/staff/ticket-view.inc.php | 190 ++++++++- .../streams/core/98ad7d55-b2ce8ba7.patch.sql | 31 ++ scp/css/scp.css | 25 ++ scp/tasks.php | 19 +- scp/tickets.php | 28 ++ setup/inc/streams/core/install-mysql.sql | 3 +- 28 files changed, 1152 insertions(+), 166 deletions(-) create mode 100644 include/staff/templates/thread-email-recipients.tmpl.php create mode 100644 include/upgrader/streams/core/98ad7d55-b2ce8ba7.patch.sql diff --git a/assets/default/css/theme.css b/assets/default/css/theme.css index 86e8dabb4..7e7027a21 100644 --- a/assets/default/css/theme.css +++ b/assets/default/css/theme.css @@ -1127,6 +1127,28 @@ img.avatar { margin-left: 1px; } +.thread-entry.bccmessage .header { + background:#DDFDAC; +} +.thread-entry.avatar.bccmessage .header:before { + top: 7px; + left: -8px; + right: initial; + border-left: none; + border-right: 8px solid #CCC; +} +.thread-entry.avatar.bccmessage .header:before { + border-right-color: #CCC; +} +.thread-entry.avatar.bccmessage .header:after { + top: 7px; + left: -8px; + right: initial; + border-left: none; + border-right: 7px solid #DDFDAC; + margin-left: 1px; +} + .thread-entry .header .title { max-width: 500px; vertical-align: bottom; diff --git a/include/ajax.content.php b/include/ajax.content.php index 38112a115..48cb6f2b0 100644 --- a/include/ajax.content.php +++ b/include/ajax.content.php @@ -101,6 +101,7 @@ class ContentAjaxAPI extends AjaxController { <tr><td>.lastmessage</td><td>'.__('Last Message').'</td></tr> <tr><td colspan="2" style="padding:5px 0 5px 0;"><em><b>'.__('Thread Entry expansions').'</b></em></td></tr> <tr><td>.poster</td><td>'.__('Poster').'</td></tr> + <tr><td>.posterType</td><td>'.__('Can be User or Agent').'</td></tr> <tr><td>.create_date</td><td>'.__('Date Created').'</td></tr> </table> </td> diff --git a/include/ajax.thread.php b/include/ajax.thread.php index 0523d851b..e1e8a2663 100644 --- a/include/ajax.thread.php +++ b/include/ajax.thread.php @@ -94,7 +94,6 @@ class ThreadAjaxAPI extends AjaxController { || !$object->checkStaffPerm($thisstaff)) Http::response(404, __('No such thread')); - $user = $uid? User::lookup($uid) : null; //If not a post then assume new collaborator form @@ -116,6 +115,8 @@ class ThreadAjaxAPI extends AjaxController { array('isactive'=>1), $errors))) { $info = array('msg' => sprintf(__('%s added as a collaborator'), Format::htmlchars($c->getName()))); + $c->setCc(); + $c->save(); return self::_collaborators($thread, $info); } } @@ -232,8 +233,7 @@ class ThreadAjaxAPI extends AjaxController { if ($thread->updateCollaborators($_POST, $errors)) Http::response(201, $this->json_encode(array( 'id' => $thread->getId(), - 'text' => sprintf('Recipients (%d of %d)', - $thread->getNumActiveCollaborators(), + 'text' => sprintf('(%d)', $thread->getNumCollaborators()) ) )); diff --git a/include/class.client.php b/include/class.client.php index 53b6376c9..36fa13746 100644 --- a/include/class.client.php +++ b/include/class.client.php @@ -59,13 +59,23 @@ implements EmailContact, ITicketUser, TemplateVariable { case 'ticket_link': $qstr = array(); if ($cfg && $cfg->isAuthTokenEnabled() - && ($ticket=$this->getTicket())) - $qstr['auth'] = $ticket->getAuthToken($this); + && ($ticket=$this->getTicket()) + && !$ticket->getThread()->getNumCollaborators()) { + $qstr['auth'] = $ticket->getAuthToken($this); + return sprintf('%s/view.php?%s', + $cfg->getBaseUrl(), + Http::build_query($qstr, false) + ); + } + else { + return sprintf('%s/tickets.php?id=%s', + $cfg->getBaseUrl(), + $ticket->getId() + ); + } + + - return sprintf('%s/view.php?%s', - $cfg->getBaseUrl(), - Http::build_query($qstr, false) - ); break; } } @@ -162,7 +172,7 @@ class TicketOwner extends TicketUser { * */ -class EndUser extends BaseAuthenticatedUser { +class EndUser extends BaseAuthenticatedUser { protected $user; protected $_account = false; diff --git a/include/class.collaborator.php b/include/class.collaborator.php index 0de95b6e4..4bd0e7cf8 100644 --- a/include/class.collaborator.php +++ b/include/class.collaborator.php @@ -34,6 +34,9 @@ implements EmailContact, ITicketUser { ), ); + const FLAG_ACTIVE = 0x0001; + const FLAG_CC = 0x0002; + function __toString() { return Format::htmlchars($this->toString()); } @@ -46,7 +49,7 @@ implements EmailContact, ITicketUser { } function isActive() { - return $this->isactive; + return !!($this->flags & self::FLAG_ACTIVE); } function getCreateDate() { @@ -80,21 +83,36 @@ implements EmailContact, ITicketUser { return $this->user->getName(); } + static function getIdByUserId($userId, $threadId) { + $row = Collaborator::objects() + ->filter(array('user_id'=>$userId, 'thread_id'=>$threadId)) + ->values_flat('id') + ->first(); + + return $row ? $row[0] : 0; + } + // VariableReplacer interface function getVar($what) { global $cfg; switch (strtolower($what)) { case 'ticket_link': - return sprintf('%s/view.php?%s', - $cfg->getBaseUrl(), - Http::build_query( - // TODO: Chance to $this->getTicket when - array('auth' => $this->getTicket()->getAuthToken($this)), - false - ) - ); - break; + if ($this->getTicket()->getAuthToken($this) + && ($ticket=$this->getTicket()) + && !$ticket->getThread()->getNumCollaborators()) { + $qstr['auth'] = $ticket->getAuthToken($this); + return sprintf('%s/view.php?%s', + $cfg->getBaseUrl(), + Http::build_query($qstr, false) + ); + } + else { + return sprintf('%s/tickets.php?id=%s', + $cfg->getBaseUrl(), + $ticket->getId() + ); + } } } @@ -114,6 +132,45 @@ implements EmailContact, ITicketUser { return $this->user_id; } + function hasFlag($flag) { + return ($this->get('flags', 0) & $flag) != 0; + } + + public function setFlag($flag, $val) { + if ($val) + $this->flags |= $flag; + else + $this->flags &= ~$flag; + } + + public function setCc() { + $this->setFlag(Collaborator::FLAG_ACTIVE, true); + $this->setFlag(Collaborator::FLAG_CC, true); + $this->save(); + } + + public function setBcc() { + $this->setFlag(Collaborator::FLAG_ACTIVE, true); + $this->setFlag(Collaborator::FLAG_CC, false); + $this->save(); + } + + function isCc() { + return !!($this->flags & self::FLAG_CC); + } + + function getCollabList($collabs) { + $collabList = array(); + foreach ($collabs as $c) { + $u = User::lookup($c); + if ($u) { + $email = $u->getEmail()->address; + $collabList[$c] = $email; + } + } + return $collabList; + } + static function create($vars=false) { $inst = new static($vars); $inst->created = SqlFunction::NOW(); @@ -127,15 +184,14 @@ implements EmailContact, ITicketUser { } static function add($info, &$errors) { - if (!$info || !$info['threadId'] || !$info['userId']) $errors['err'] = __('Invalid or missing information'); - elseif ($c = static::lookup(array( + elseif ($c = Collaborator::lookup(array( 'thread_id' => $info['threadId'], 'user_id' => $info['userId'], ))) - $errors['err'] = sprintf(__('%s is already a collaborator'), - $c->getName()); + $errors['err'] = sprintf(__('%s is already a collaborator'), + $c->getName()); if ($errors) return false; diff --git a/include/class.email.php b/include/class.email.php index c1542113e..a91bebde5 100644 --- a/include/class.email.php +++ b/include/class.email.php @@ -169,13 +169,13 @@ class Email extends VerySimpleModel { return $info; } - function send($to, $subject, $message, $attachments=null, $options=null) { + function send($to, $subject, $message, $attachments=null, $options=null, $cc=array()) { $mailer = new Mailer($this); if($attachments) $mailer->addAttachments($attachments); - return $mailer->send($to, $subject, $message, $options); + return $mailer->send($to, $subject, $message, $options, $cc); } function sendAutoReply($to, $subject, $message, $attachments=null, $options=array()) { diff --git a/include/class.mailer.php b/include/class.mailer.php index a97cf650f..ed1ef55df 100644 --- a/include/class.mailer.php +++ b/include/class.mailer.php @@ -294,7 +294,7 @@ class Mailer { 0, 6); } - function send($recipient, $subject, $message, $options=null) { + function send($recipient, $subject, $message, $options=null, $collabs=array()) { global $ost, $cfg; //Get the goodies @@ -508,6 +508,19 @@ class Mailer { } } + $cc = array(); + if($collabs) { + if($collabs['cc']) { + foreach ($collabs['cc'] as $email) { + $mime->addCc($email); + $email = preg_replace("/(\r\n|\r|\n)/s",'', trim($email)); + $cc[] = $email; + } + $to = $to.', '.implode(', ',$cc); + } + } + $to = ltrim($to, ', '); + //Desired encodings... $encodings=array( 'head_encoding' => 'quoted-printable', diff --git a/include/class.task.php b/include/class.task.php index 4b98ff1d4..7cb0aab79 100644 --- a/include/class.task.php +++ b/include/class.task.php @@ -1150,6 +1150,14 @@ class Task extends TaskModel implements RestrictedAccess, Threadable { } + function addCollaborator($user, $vars, &$errors, $event=true) { + if ($c = $this->getThread()->addCollaborator($user, $vars, $errors, $event)) { + $this->collaborators = null; + $this->recipients = null; + } + return $c; + } + /* * Notify collaborators on response or new message * diff --git a/include/class.thread.php b/include/class.thread.php index af92d3a5f..f101aef0d 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -118,7 +118,13 @@ implements Searchable { } function getActiveCollaborators() { - return $this->getCollaborators(array('isactive'=>1)); + $collaborators = $this->getCollaborators(); + $active = array(); + foreach ($collaborators as $c) { + if ($c->isactive()) + $active[] = $c; + } + return $active; } function getCollaborators($criteria=array()) { @@ -130,7 +136,8 @@ implements Searchable { ->filter(array('thread_id' => $this->getId())); if (isset($criteria['isactive'])) - $collaborators->filter(array('isactive' => $criteria['isactive'])); + $collaborators->filter(array('flags__hasbit'=>Collaborator::FLAG_ACTIVE)); + // TODO: sort by name of the user $collaborators->order_by('user__name'); @@ -177,13 +184,14 @@ implements Searchable { $collabs = array(); foreach ($ids as $k => $cid) { if (($c=Collaborator::lookup($cid)) - && $c->getThreadId() == $this->getId() + && ($c->getThreadId() == $this->getId()) && $c->delete()) $collabs[] = $c; + + $this->getEvents()->log($this->getObject(), 'collab', array( + 'del' => array($c->user_id => array('name' => $c->getName()->getOriginal())) + )); } - $this->getEvents()->log($this->getObject(), 'collab', array( - 'del' => array($c->user_id => array('name' => $c->getName()->getOriginal())) - )); } //statuses @@ -196,15 +204,42 @@ implements Searchable { 'updated' => SqlFunction::NOW(), 'isactive' => 1, )); + + foreach ($vars['cid'] as $c) { + $collab = Collaborator::lookup($c); + if(get_class($collab) == 'Collaborator') { + $collab->setFlag(Collaborator::FLAG_ACTIVE, true); + $collab->save(); + } + } } - $this->collaborators->filter(array( + $inactive = $this->collaborators->filter(array( 'thread_id' => $this->getId(), Q::not(array('id__in' => $cids ?: array(0))) - ))->update(array( - 'updated' => SqlFunction::NOW(), - 'isactive' => 0, )); + if($inactive) { + foreach ($inactive as $i) { + $i->setFlag(Collaborator::FLAG_ACTIVE, false); + $i->save(); + } + $inactive->update(array( + 'updated' => SqlFunction::NOW(), + 'isactive' => 0, + )); + } + + if($vars['recipientType']) { + $combo = array_combine($vars['uid'], $vars['recipientType']); + foreach ($combo as $id => $type) { + $collab = Collaborator::lookup($id); + if(get_class($collab) == 'Collaborator') { + $type == 'Cc' ? $collab->setFlag(Collaborator::FLAG_CC, true) : + $collab->setFlag(Collaborator::FLAG_CC, false); + $collab->save(); + } + } + } unset($this->ht['active_collaborators']); $this->_collaborators = null; @@ -240,8 +275,17 @@ implements Searchable { include_once INCLUDE_DIR . 'class.thread_actions.php'; $entries = $this->getEntries(); - if ($type && is_array($type)) - $entries->filter(array('type__in' => $type)); + + if ($type && is_array($type)) { + $visibility = Q::all(array('type__in' => $type)); + + if ($type['poster']) { + $visibility->add(array('poster__exact' => $type['poster'])); + $visibility->ored = true; + } + + $entries->filter($visibility); + } if ($options['sort'] && !strcasecmp($options['sort'], 'DESC')) $entries->order_by('-id'); @@ -329,6 +373,30 @@ implements Searchable { $body = $mailinfo['message']; + // extra handling for determining Cc and Bcc collabs + if ($mailinfo['email']) { + $staffSenderId = Staff::getIdByEmail($mailinfo['email']); + + if (!$staffSenderId) { + $senderId = UserEmailModel::getIdByEmail($mailinfo['email']); + if ($senderId) { + $mailinfo['userId'] = $senderId; + + if ($object instanceof Ticket && $senderId != $object->user_id && $senderId != $object->staff_id) { + $mailinfo['userClass'] = 'C'; + + $collaboratorId = Collaborator::getIdByUserId($senderId, $this->getId()); + $collaborator = Collaborator::lookup($collaboratorId); + + if ($collaborator && ($collaborator->isCc())) + $vars['thread-type'] = 'M'; + else + $vars['thread-type'] = 'N'; + } + } + } + } + // Attempt to determine the user posting the entry and the // corresponding entry type by the information determined by the // mail parser (via the In-Reply-To header) @@ -403,7 +471,6 @@ implements Searchable { switch ($vars['thread-type']) { case 'M': $vars['message'] = $body; - if ($object instanceof Threadable) return $object->postThreadEntry('M', $vars); elseif ($this instanceof ObjectThread) @@ -412,7 +479,6 @@ implements Searchable { case 'N': $vars['note'] = $body; - if ($object instanceof Threadable) return $object->postThreadEntry('N', $vars); elseif ($this instanceof ObjectThread) @@ -622,6 +688,7 @@ implements TemplateVariable { var $_actions; var $is_autoreply; var $is_bounce; + var $_posterType; static protected $perms = array( self::PERM_EDIT => array( @@ -688,6 +755,13 @@ implements TemplateVariable { return $this->poster; } + function getPosterType() { + $this->staff_id ? + $this->posterType = __('Agent') : $this->posterType = __('User'); + + return $this->posterType; + } + function getTitle() { return $this->title; } @@ -1314,7 +1388,6 @@ implements TemplateVariable { if (!$vars['threadId'] || !$vars['type']) return false; - if (!$vars['body'] instanceof ThreadEntryBody) { if ($cfg->isRichTextEnabled()) $vars['body'] = new HtmlThreadEntryBody($vars['body']); @@ -1340,8 +1413,40 @@ implements TemplateVariable { 'poster' => $poster, 'source' => $vars['source'], 'flags' => $vars['flags'] ?: 0, + 'recipients' => $vars['recipients'], )); + //add recipients to thread entry + $recipients = array(); + $ticket = Thread::objects()->filter(array('id'=>$vars['threadId']))->values_flat('object_id')->first(); + $ticketUser = Ticket::objects()->filter(array('ticket_id'=>$ticket[0]))->values_flat('user_id')->first(); + + //User + if ($ticketUser) { + $uEmail = UserEmailModel::objects()->filter(array('user_id'=>$ticketUser[0]))->values_flat('address')->first(); + $u = array(); + $u[$ticketUser[0]] = $uEmail[0]; + $recipients['to'] = $u; + } + + if (Collaborator::getIdByUserId($vars['userId'], $vars['threadId'])) + $entry->flags |= ThreadEntry::FLAG_COLLABORATOR; + + //Cc collaborators + if($vars['ccs'] && $vars['emailcollab'] == 1) { + $cc = Collaborator::getCollabList($vars['ccs']); + $recipients['cc'] = $cc; + } + + //Bcc Collaborators + if($vars['bccs'] && $vars['emailcollab'] == 1) { + $bcc = Collaborator::getCollabList($vars['bccs']); + $recipients['bcc'] = $bcc; + } + + if (($vars['do'] == 'create' || $vars['emailreply'] == 1) && $recipients) + $entry->recipients = json_encode($recipients); + if ($entry->format == 'html') // The current codebase properly balances html $entry->flags |= self::FLAG_BALANCED; @@ -1898,7 +2003,7 @@ class CollaboratorEvent extends ThreadEvent { } $desc = sprintf($base, implode(', ', $collabs)); break; - case isset($data['add']): + case isset($data['add']) && $mode!=self::MODE_CLIENT: $base = __('<b>{somebody}</b> added <strong>%s</strong> as collaborators {timestamp}'); $collabs = array(); if ($data['add']) { @@ -2521,14 +2626,12 @@ implements TemplateVariable { } function addNote($vars, &$errors=array()) { - //Add ticket Id. $vars['threadId'] = $this->getId(); return NoteThreadEntry::add($vars, $errors); } function addMessage($vars, &$errors) { - $vars['threadId'] = $this->getId(); $vars['staffId'] = 0; diff --git a/include/class.thread_actions.php b/include/class.thread_actions.php index 1eaab156a..dffc93631 100644 --- a/include/class.thread_actions.php +++ b/include/class.thread_actions.php @@ -18,6 +18,65 @@ **********************************************************************/ include_once(INCLUDE_DIR.'class.thread.php'); +class TEA_ShowEmailRecipients extends ThreadEntryAction { + static $id = 'emailrecipients'; + static $name = /* trans */ 'View Email Recipients'; + static $icon = 'group'; + + function isVisible() { + global $thisstaff; + + if ($this->entry->getEmailHeader()) + return ($thisstaff && $this->entry->getEmailHeader()); + elseif ($this->entry->recipients) + return $this->entry->recipients; + + } + + function getJsStub() { + return sprintf("$.dialog('%s');", + $this->getAjaxUrl() + ); + } + + function trigger() { + switch ($_SERVER['REQUEST_METHOD']) { + case 'GET' && $this->entry->recipients: + return $this->getRecipients(); + case 'GET': + return $this->trigger__get(); + } + } + + private function trigger__get() { + $hdr = Mail_parse::splitHeaders( + $this->entry->getEmailHeader(), true); + + $recipients = array(); + foreach (array('To', 'TO', 'Cc', 'CC', 'Bcc', 'BCC') as $k) { + if (isset($hdr[$k]) && $hdr[$k] && + ($addresses=Mail_Parse::parseAddressList($hdr[$k]))) { + foreach ($addresses as $addr) { + $email = sprintf('%s@%s', $addr->mailbox, $addr->host); + $name = $addr->personal ?: ''; + $recipients[$k][] = sprintf('%s<%s>', + (($name && strcasecmp($name, $email))? "$name ": ''), + $email); + } + } + } + + include STAFFINC_DIR . 'templates/thread-email-recipients.tmpl.php'; + } + + private function getRecipients() { + $recipients = json_decode($this->entry->recipients, true); + + include STAFFINC_DIR . 'templates/thread-email-recipients.tmpl.php'; + } +} +ThreadEntry::registerAction(/* trans */ 'E-Mail', 'TEA_ShowEmailRecipients'); + class TEA_ShowEmailHeaders extends ThreadEntryAction { static $id = 'view_headers'; static $name = /* trans */ 'View Email Headers'; @@ -134,6 +193,7 @@ JS 'staffId' => $old->staff_id, 'type' => $old->type, 'threadId' => $old->thread_id, + 'recipients' => $old->recipients, // Connect the new entry to be a child of the previous 'pid' => $old->id, @@ -313,16 +373,19 @@ class TEA_EditAndResendThreadEntry extends TEA_EditThreadEntry { && ($tpl = $dept->getTemplate()) && ($msg=$tpl->getReplyMsgTemplate())) { + $recipients = json_decode($response->recipients, true); + $msg = $object->replaceVars($msg->asArray(), $variables + array('recipient' => $object->getOwner())); $attachments = $cfg->emailAttachments() ? $response->getAttachments() : array(); $email->send($object->getOwner(), $msg['subj'], $msg['body'], - $attachments, $options); + $attachments, $options, $recipients); } // TODO: Add an option to the dialog - $object->notifyCollaborators($response, array('signature' => $signature)); + if ($object instanceof Task) + $object->notifyCollaborators($response, array('signature' => $signature)); // Log an event that the item was resent $object->logEvent('resent', array('entry' => $response->id)); diff --git a/include/class.ticket.php b/include/class.ticket.php index 4d9251c66..e6c85da6b 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -778,16 +778,35 @@ implements RestrictedAccess, Threadable, Searchable { } //UserList of recipients (owner + collaborators) - function getRecipients() { - if (!isset($this->recipients)) { - $list = new UserList(); - $list->add($this->getOwner()); - if ($collabs = $this->getThread()->getActiveCollaborators()) { - foreach ($collabs as $c) + function getRecipients($excludeBcc=false) { + if ($excludeBcc && isset($this->recipients)) { + $list = new UserList(); + + if ($collabs = $this->getThread()->getActiveCollaborators()) { + $list->add($this->getOwner()); + foreach ($collabs as $c) { + if (get_class($c) == 'Collaborator' && !$c->isCc()) //skip bcc + continue; + else $list->add($c); - } - $this->recipients = $list; + } + } + + $this->recipients = $list; + } + //I think we need to rebuild each time since it + //would be incomplete if called after an exclude bcc call + else { + $list = new UserList(); + $list->add($this->getOwner()); + if ($collabs = $this->getThread()->getActiveCollaborators()) { + foreach ($collabs as $c) { + $list->add($c); + } + } + $this->recipients = $list; } + return $this->recipients; } @@ -931,6 +950,7 @@ implements RestrictedAccess, Threadable, Searchable { 'updated' => SqlFunction::NOW(), 'isactive' => 1, )); + $collab->save(); } if ($cids) { @@ -1386,8 +1406,7 @@ implements RestrictedAccess, Threadable, Searchable { * Notify collaborators on response or new message * */ - - function notifyCollaborators($entry, $vars = array()) { + function notifyCollaborators($entry, $vars = array()) { global $cfg; if (!$entry instanceof ThreadEntry @@ -1399,25 +1418,22 @@ implements RestrictedAccess, Threadable, Searchable { ) { return; } - // Who posted the entry? - $skip = array(); - if ($entry instanceof MessageThreadEntry) { - $poster = $entry->getUser(); - // Skip the person who sent in the message - $skip[$entry->getUserId()] = 1; - // Skip all the other recipients of the message - foreach ($entry->getAllEmailRecipients() as $R) { - foreach ($recipients as $R2) { - if (0 === strcasecmp($R2->getEmail(), $R->mailbox.'@'.$R->host)) { - $skip[$R2->getUserId()] = true; - break; - } - } - } - } else { - $poster = $entry->getStaff(); - // Skip the ticket owner - $skip[$this->getUserId()] = 1; + + $poster = User::lookup($entry->user_id); + $posterEmail = $poster->getEmail()->address; + + if($vars['ccs']) { + foreach ($vars['ccs'] as $cc) { + $collab = Collaborator::getIdByUserId($cc, $this->getThread()->getId()); + $recipients[] = Collaborator::lookup($collab); + } + } + + if($vars['bccs']) { + foreach ($vars['bccs'] as $bcc) { + $collab = Collaborator::getIdByUserId($bcc, $this->getThread()->getId()); + $recipients[] = Collaborator::lookup($collab); + } } $vars = array_merge($vars, array( @@ -1434,15 +1450,53 @@ implements RestrictedAccess, Threadable, Searchable { if ($vars['from_name']) $options += array('from_name' => $vars['from_name']); + $skip = array(); + if ($entry instanceof MessageThreadEntry) { + foreach ($entry->getAllEmailRecipients() as $R) { + $skip[] = $R->mailbox.'@'.$R->host; + } + } + + $collaborators = array(); + $collabsCc = array(); + $collabsBcc = array(); foreach ($recipients as $recipient) { - // Skip folks who have already been included on this part of - // the conversation - if (isset($skip[$recipient->getUserId()])) - continue; - $notice = $this->replaceVars($msg, array('recipient' => $recipient)); - $email->send($recipient, $notice['subj'], $notice['body'], $attachments, - $options); + if(get_class($recipient) == 'Collaborator') { + if ($recipient->isCc()) { + $collabsCc[] = $recipient->getEmail()->address; + $cnotice = $this->replaceVars($msg, array('recipient.name.first' => __('Collaborator'), 'recipient' => $recipient)); + } + else + $collabsBcc[] = $recipient; + } + + if(get_class($recipient) == 'TicketOwner') { + $owner = $recipient; + } + } + + foreach ($collabsBcc as $recipient) { + $notice = $this->replaceVars($msg, array('recipient' => $recipient)); + if ($posterEmail != $recipient->getEmail()->address) + $email->send($recipient, $notice['subj'], $notice['body'], $attachments, + $options); + } + + foreach ($collabsCc as $cc) { + if (in_array($cc, $skip)) + continue; + elseif ($cc != $posterEmail) + $collaborators[] = $cc; } + + if ($owner->getEmail()->address != $poster->getEmail()->address && !in_array($owner->getEmail()->address, $skip)) + $collaborators[] = $owner->getEmail()->address; + + $collaborators['cc'] = $collaborators; + + //collaborator email sent out + $email->send('', $cnotice['subj'], $cnotice['body'], $attachments, + $options, $collaborators); } function onMessage($message, $autorespond=true, $reopen=true) { @@ -1534,7 +1588,6 @@ implements RestrictedAccess, Threadable, Searchable { global $cfg, $thisstaff; //TODO: do some shit - if (!$alert // Check if alert is enabled || !$cfg->alertONNewActivity() || !($dept=$this->getDept()) @@ -1933,6 +1986,8 @@ implements RestrictedAccess, Threadable, Searchable { function replaceVars($input, $vars = array()) { global $ost; + $recipients = $this->getRecipients(true); + $vars = array_merge($vars, array('ticket' => $this)); return $ost->replaceTemplateVariables($input, $vars); } @@ -2276,6 +2331,69 @@ implements RestrictedAccess, Threadable, Searchable { $vars['ip_address'] = $_SERVER['REMOTE_ADDR']; $errors = array(); + + $hdr = Mail_parse::splitHeaders($vars['header'], true); + $existingCollab = Collaborator::getIdByUserId($vars['userId'], $this->getThreadId()); + + if (($vars['userId'] != $this->user_id) && (!$existingCollab)) { + if ($vars['userId'] == 0) { + $emailStream = '<<<EOF' . $vars['header'] . 'EOF'; + $parsed = EmailDataParser::parse($emailStream); + $email = $parsed['email']; + if (!$existinguser = User::lookupByEmail($email)) { + $name = $parsed['name']; + $user = User::fromVars(array('name' => $name, 'email' => $email)); + $vars['userId'] = $user->getId(); + } + } + else + $user = User::lookup($vars['userId']); + + $c = $this->getThread()->addCollaborator($user,array('isactive'=>1), $errors); + + foreach (array('To', 'TO', 'Cc', 'CC') as $k) { + if ($user && isset($hdr[$k]) && $hdr[$k]) + $addresses[] = Mail_Parse::parseAddressList($hdr[$k]); + } + if (count($addresses) > 1) { + $isMsg = true; + $c->setCc(); + } + else + $c->setBcc(); + } + else { + $c = Collaborator::lookup($existingCollab); + if ($c && !$c->isCc()) { + foreach (array('To', 'TO', 'Cc', 'CC') as $k) { + if (isset($hdr[$k]) && $hdr[$k]) + $addresses[] = Mail_Parse::parseAddressList($hdr[$k]); + } + if (count($addresses) > 1) { + $isMsg = true; + $c->setCc(); + } + } + } + + if ($vars['userId'] == $this->user_id) + $isMsg = true; + + //lookup user by userId. if they are bcc in thread, post internal note + if($collabs = $this->getRecipients()) { + foreach ($collabs as $collab) { + if(get_class($collab) == 'Collaborator' && $collab->user_id == $vars['userId'] && !$collab->isCc()) { + $user = User::lookup($vars['userId']); + $vars['note'] = $vars['message']; + + //post internal note + if (!$isMsg) { + return $this->postNote($vars,$errors, $user, true); + } + } + } + } + if (!($message = $this->getThread()->addMessage($vars, $errors))) return null; @@ -2297,14 +2415,22 @@ implements RestrictedAccess, Threadable, Searchable { if (strcasecmp($recipient['source'], 'delivered-to') === 0) continue; - if (($user=User::fromVars($recipient))) - if ($c=$this->addCollaborator($user, $info, $errors, false)) - // FIXME: This feels very unwise — should be a - // string indexed array for future - $collabs[$c->user_id] = array( - 'name' => $c->getName()->getOriginal(), - 'src' => $recipient['source'], - ); + if (($cuser=User::fromVars($recipient))) { + if (!$existing = Collaborator::getIdByUserId($cuser->getId(), $this->getThreadId())) { + if ($c=$this->addCollaborator($cuser, $info, $errors, false)) { + $c->setCc(); + + // FIXME: This feels very unwise — should be a + // string indexed array for future + $collabs[$c->user_id] = array( + 'name' => $c->getName()->getOriginal(), + 'src' => $recipient['source'], + ); + } + } + + } + } // TODO: Can collaborators add others? if ($collabs) { @@ -2324,8 +2450,10 @@ implements RestrictedAccess, Threadable, Searchable { $this->onMessage($message, ($autorespond && $alerts), $reopen); //must be called b4 sending alerts to staff. - if ($autorespond && $alerts && $cfg && $cfg->notifyCollabsONNewMessage()) - $this->notifyCollaborators($message, array('signature' => '')); + if ($autorespond && $alerts && $cfg && $cfg->notifyCollabsONNewMessage()) { + //when user replies, this is where collabs notified + $this->notifyCollaborators($message, array('signature' => '')); + } if (!($alerts && $autorespond)) return $message; //Our work is done... @@ -2389,7 +2517,6 @@ implements RestrictedAccess, Threadable, Searchable { $sentlist[] = $staff->getEmail(); } } - return $message; } @@ -2469,6 +2596,30 @@ implements RestrictedAccess, Threadable, Searchable { function postReply($vars, &$errors, $alert=true, $claim=true) { global $thisstaff, $cfg; + if ($collabs = $this->getRecipients()) { + $collabIds = array(); + foreach ($collabs as $collab) + $collabIds[] = $collab->user_id; + } + + $ticket = Ticket::lookup($vars['id']); + if (isset($vars['ccs'])) { + foreach ($vars['ccs'] as $uid) { + $user = User::lookup($uid); + if (!in_array($uid, $collabIds)) + if (($c2=$ticket->getThread()->addCollaborator($user,array('isactive'=>1), $errors))) + $c2->setCc(); + } + } + if (isset($vars['bccs'])) { + foreach ($vars['bccs'] as $uid) { + $user = User::lookup($uid); + if (!in_array($uid, $collabIds)) + if (($c2=$ticket->getThread()->addCollaborator($user,array('isactive'=>1), $errors))) + $c2->setBcc(); + } + } + if (!$vars['poster'] && $thisstaff) $vars['poster'] = $thisstaff; @@ -2507,7 +2658,9 @@ implements RestrictedAccess, Threadable, Searchable { if (!$alert) return $response; - $email = $dept->getEmail(); + //allow agent to send from different dept email + $vars['from_name'] ? $email = Email::lookup($vars['from_name']) : $email = $dept->getEmail(); + $options = array('thread'=>$response); $signature = $from_name = ''; if ($thisstaff && $vars['signature']=='mine') @@ -2529,10 +2682,8 @@ implements RestrictedAccess, Threadable, Searchable { default: $from_name = $email->getName(); } - if ($from_name) $options += array('from_name' => $from_name); - } $variables = array( @@ -2543,7 +2694,7 @@ implements RestrictedAccess, Threadable, Searchable { ); $user = $this->getOwner(); - if (($email=$dept->getEmail()) + if (($email=$email) && ($tpl = $dept->getTemplate()) && ($msg=$tpl->getReplyMsgTemplate()) ) { @@ -2551,17 +2702,42 @@ implements RestrictedAccess, Threadable, Searchable { $variables + array('recipient' => $user) ); $attachments = $cfg->emailAttachments()?$response->getAttachments():array(); + } + + if ($vars['emailcollab'] == 1) { + //Cc collaborators + if($vars['ccs']) { + $collabsCc = array(); + $collabsCc[] = Collaborator::getCollabList($vars['ccs']); + $collabsCc['cc'] = $collabsCc; + $email->send($user, $msg['subj'], $msg['body'], $attachments, + $options, $collabsCc); + } + else { + $email->send($user, $msg['subj'], $msg['body'], $attachments, + $options); + } + + //Bcc Collaborators + if($vars['bccs']) { + foreach ($vars['bccs'] as $uid) { + $recipient = User::lookup($uid); + if (($bcctpl = $dept->getTemplate()) + && ($bccmsg=$bcctpl->getReplyMsgTemplate())) { + $bccmsg = $this->replaceVars($bccmsg->asArray(), $variables + + array('recipient' => $user, 'recipient.name.first' => $recipient->getName()->getFirst()) + ); + + $email->send($recipient, $bccmsg['subj'], $bccmsg['body'], $attachments, + $options); + } + } + } + } + else $email->send($user, $msg['subj'], $msg['body'], $attachments, $options); - } - if ($vars['emailcollab']) { - $this->notifyCollaborators($response, - array( - 'signature' => $signature, - 'from_name' => $from_name) - ); - } return $response; } @@ -2596,12 +2772,12 @@ implements RestrictedAccess, Threadable, Searchable { function postNote($vars, &$errors, $poster=false, $alert=true) { global $cfg, $thisstaff; - //Who is posting the note - staff or system? + //Who is posting the note - staff or system? or user? if ($vars['staffId'] && !$poster) $poster = Staff::lookup($vars['staffId']); $vars['staffId'] = $vars['staffId'] ?: 0; - if ($poster && is_object($poster)) { + if ($poster && is_object($poster) && !$vars['userId']) { $vars['staffId'] = $poster->getId(); $vars['poster'] = $poster->getName(); } @@ -3546,6 +3722,35 @@ implements RestrictedAccess, Threadable, Searchable { if (!($ticket=self::create($create_vars, $errors, 'staff', false))) return false; + $collabsCc = array(); + $collabsBcc = array(); + if (isset($vars['ccs'])) { + foreach ($vars['ccs'] as $uid) { + $ccuser = User::lookup($uid); + + if ($ccuser && !$existing = Collaborator::getIdByUserId($ccuser->getId(), $ticket->getThreadId())) { + $collabsCc[] = $ccuser->getEmail()->address; + + if (($c2=$ticket->getThread()->addCollaborator($ccuser,array('isactive'=>1), $errors))) + $c2->setCc(); + } + } + $collabsCc['cc'] = $collabsCc; + } + + if (isset($vars['bccs'])) { + foreach ($vars['bccs'] as $uid) { + $bccuser = User::lookup($uid); + + if ($bccuser && !$existing = Collaborator::getIdByUserId($bccuser->getId(), $ticket->getThreadId())) { + $collabsBcc[] = $bccuser; + + if (($c2=$ticket->getThread()->addCollaborator($bccuser,array('isactive'=>1), $errors))) + $c2->setBcc(); + } + } + } + $vars['msgId']=$ticket->getLastMsgId(); // Effective role for the department @@ -3608,8 +3813,37 @@ implements RestrictedAccess, Threadable, Searchable { $options = array( 'thread' => $message ?: $ticket->getThread(), ); - $email->send($ticket->getOwner(), $msg['subj'], $msg['body'], $attachments, - $options); + + //ticket created on user's behalf + if($vars['emailcollab'] == 1) { + + $email->send($ticket->getOwner(), $msg['subj'], $msg['body'], $attachments, + $options, $collabsCc); + + if ($collabsBcc) { + foreach ($collabsBcc as $recipient) { + if (($tpl=$dept->getTemplate()) + && ($bccmsg=$tpl->getNewTicketNoticeMsgTemplate()) + && ($email=$dept->getEmail()) + ) + $bccmsg = $ticket->replaceVars($bccmsg->asArray(), + array( + 'message' => $message, + 'signature' => $signature, + 'response' => ($response) ? $response->getBody() : '', + 'recipient' => $ticket->getOwner(), + 'recipient.name.first' => $recipient->getName()->getFirst(), + ) + ); + + $email->send($recipient, $bccmsg['subj'], $bccmsg['body'], $attachments, + $options); + } + } + } + else + $email->send($ticket->getOwner(), $msg['subj'], $msg['body'], $attachments, + $options); } return $ticket; } diff --git a/include/class.user.php b/include/class.user.php index e5f898d7d..4edb3d7a2 100644 --- a/include/class.user.php +++ b/include/class.user.php @@ -34,6 +34,15 @@ class UserEmailModel extends VerySimpleModel { function __toString() { return (string) $this->address; } + + static function getIdByEmail($email) { + $row = UserEmailModel::objects() + ->filter(array('address'=>$email)) + ->values_flat('user_id') + ->first(); + + return $row ? $row[0] : 0; + } } class UserModel extends VerySimpleModel { @@ -149,6 +158,13 @@ class UserModel extends VerySimpleModel { return true; } + public function setFlag($flag, $val) { + if ($val) + $this->status |= $flag; + else + $this->status &= ~$flag; + } + protected function hasStatus($flag) { return $this->get('status') & $flag !== 0; } diff --git a/include/client/templates/thread-entry.tmpl.php b/include/client/templates/thread-entry.tmpl.php index c87edfa0b..3ab3127e8 100644 --- a/include/client/templates/thread-entry.tmpl.php +++ b/include/client/templates/thread-entry.tmpl.php @@ -1,16 +1,22 @@ <?php global $cfg; -$entryTypes = array('M'=>'message', 'R'=>'response', 'N'=>'note'); +$entryTypes = array('M'=>'message', 'R'=>'response', 'N'=>'note', 'B' => 'bccmessage'); $user = $entry->getUser() ?: $entry->getStaff(); $name = $user ? $user->getName() : $entry->poster; $avatar = ''; if ($cfg->isAvatarsEnabled() && $user) $avatar = $user->getAvatar(); ?> +<?php + if ($entryTypes[$entry->type] == 'note') { + $entryTypes[$entry->type] = 'bccmessage'; + $entry->type = 'B'; + } +?> <div class="thread-entry <?php echo $entryTypes[$entry->type]; ?> <?php if ($avatar) echo 'avatar'; ?>"> <?php if ($avatar) { ?> - <span class="<?php echo ($entry->type == 'M') ? 'pull-left' : 'pull-right'; ?> avatar"> + <span class="<?php echo ($entry->type == 'M' || $entry->type == 'B') ? 'pull-left' : 'pull-right'; ?> avatar"> <?php echo $avatar; ?> </span> <?php } ?> diff --git a/include/client/templates/thread-event.tmpl.php b/include/client/templates/thread-event.tmpl.php index 42fd8027e..faa922f6d 100644 --- a/include/client/templates/thread-event.tmpl.php +++ b/include/client/templates/thread-event.tmpl.php @@ -1,5 +1,5 @@ <?php -$desc = $event->getDescription(ThreadEvent::MODE_CLIENT); +$desc = $event->getDescription(ThreadEvent::MODE_CLIENT); if (!$desc) return; ?> diff --git a/include/client/tickets.inc.php b/include/client/tickets.inc.php index 6840c252b..28c7605bb 100644 --- a/include/client/tickets.inc.php +++ b/include/client/tickets.inc.php @@ -129,7 +129,7 @@ $tickets->order_by($order.$order_by); $tickets->values( 'ticket_id', 'number', 'created', 'isanswered', 'source', 'status_id', 'status__state', 'status__name', 'cdata__subject', 'dept_id', - 'dept__name', 'dept__ispublic', 'user__default_email__address' + 'dept__name', 'dept__ispublic', 'user__default_email__address', 'user_id' ); ?> @@ -238,6 +238,7 @@ if ($closedTickets) {?> $subject="<b>$subject</b>"; $ticketNumber="<b>$ticketNumber</b>"; } + $thisclient->getId() != $T['user_id'] ? $isCollab = true : $isCollab = false; ?> <tr id="<?php echo $T['ticket_id']; ?>"> <td> @@ -247,7 +248,11 @@ if ($closedTickets) {?> <td><?php echo Format::date($T['created']); ?></td> <td><?php echo $status; ?></td> <td> + <?php if ($isCollab) {?> + <div style="max-height: 1.2em; max-width: 320px;" class="link truncate" href="tickets.php?id=<?php echo $T['ticket_id']; ?>"><i class="icon-group"></i> <?php echo $subject; ?></div> + <?php } else {?> <div style="max-height: 1.2em; max-width: 320px;" class="link truncate" href="tickets.php?id=<?php echo $T['ticket_id']; ?>"><?php echo $subject; ?></div> + <?php } ?> </td> <td><span class="truncate"><?php echo $dept; ?></span></td> </tr> diff --git a/include/client/view.inc.php b/include/client/view.inc.php index 2aa01d019..b368ae25d 100644 --- a/include/client/view.inc.php +++ b/include/client/view.inc.php @@ -38,8 +38,17 @@ if ($thisclient && $thisclient->isGuest() </b> <small>#<?php echo $ticket->getNumber(); ?></small> <div class="pull-right"> - <a class="action-button" href="tickets.php?a=print&id=<?php - echo $ticket->getId(); ?>"><i class="icon-print"></i> <?php echo __('Print'); ?></a> + <?php + if($collabs = $ticket->getRecipients()) { + foreach ($collabs as $collab) { + if(get_class($collab) == 'Collaborator' && $collab->user_id == $thisclient->getId() && !$collab->isCc()) { + $viewThreads = true; + } + } + } ?> + <a class="action-button" href="tickets.php?a=print&id=<?php + echo $ticket->getId(); ?>"><i class="icon-print"></i> <?php echo __('Print'); ?></a> + <?php if ($ticket->hasClientEditableFields() // Only ticket owners can edit the ticket details (and other forms) && $thisclient->getId() == $ticket->getUserId()) { ?> @@ -134,13 +143,15 @@ echo $v; </tr> </table> <br> + <?php + $email = $thisclient->getUserName(); + $clientName = TicketUser::lookupByEmail($email)->getName()->name; -<?php - $ticket->getThread()->render(array('M', 'R'), array( - 'mode' => Thread::MODE_CLIENT, - 'html-id' => 'ticketThread') - ); -?> + $ticket->getThread()->render(array('M', 'R', 'poster' => $clientName), array( + 'mode' => Thread::MODE_CLIENT, + 'html-id' => 'ticketThread') + ); + ?> <div class="clear" style="padding-bottom:10px;"></div> <?php if($errors['err']) { ?> diff --git a/include/i18n/en_US/templates/email/message.alert.yaml b/include/i18n/en_US/templates/email/message.alert.yaml index e6e83e5bb..13826fba8 100644 --- a/include/i18n/en_US/templates/email/message.alert.yaml +++ b/include/i18n/en_US/templates/email/message.alert.yaml @@ -26,7 +26,7 @@ body: | <strong>From</strong>: </td> <td> - %{ticket.name} <%{ticket.email}> + %{poster.name} <%{ticket.email}> </td> </tr> <tr> diff --git a/include/staff/templates/collaborators.tmpl.php b/include/staff/templates/collaborators.tmpl.php index 4393ba1f2..7f80a5804 100644 --- a/include/staff/templates/collaborators.tmpl.php +++ b/include/staff/templates/collaborators.tmpl.php @@ -3,46 +3,73 @@ <?php if($info && $info['msg']) { echo sprintf('<p id="msg_notice" style="padding-top:2px;">%s</p>', $info['msg']); -} ?> +} + +if ($thread->object_type == 'T') + $type = '\'tickets\''; +if ($thread->object_type == 'A') + $type = '\'tasks\''; +?> <hr/> <?php if(($users=$thread->getCollaborators())) {?> <div id="manage_collaborators"> -<form method="post" class="collaborators" action="#thread/<?php echo $thread->getId(); ?>/collaborators"> +<form method="post" class="collaborators" onsubmit="refreshAndClose(<?php echo $thread->object_id; ?>, <?php echo $type; ?>);" action="#thread/<?php echo $thread->getId(); ?>/collaborators"> <table border="0" cellspacing="1" cellpadding="1" width="100%"> <?php foreach($users as $user) { $checked = $user->isActive() ? 'checked="checked"' : ''; + $cc = $user->isCc() ? 'selected="selected"' : ''; + $bcc = !$user->isCc() ? 'selected="selected"' : ''; + echo sprintf('<tr> <td> <label class="inline checkbox"> + <input type="checkbox" class="hidden" name="uid[]" id="%d" value="%d" checked="checked"> <input type="checkbox" name="cid[]" id="c%d" value="%d" %s> </label> <a class="collaborator" href="#thread/%d/collaborators/%d/view">%s%s</a> - <span class="faded"><em>%s</em></span></td> - <td width="10"> - <input type="hidden" name="del[]" id="d%d" value=""> - <a class="remove" href="#d%d">×</a></td> - <td width="30"> </td> - </tr>', - $user->getId(), - $user->getId(), - $checked, - $thread->getId(), - $user->getId(), - (($U = $user->getUser()) && ($A = $U->getAvatar())) - ? $U->getAvatar()->getImageTag(24) : '', - Format::htmlchars($user->getName()), - $user->getEmail(), - $user->getId(), - $user->getId()); + <div align="left"> + <span class="faded"><em>%s</em></span> + </div> + </td>', $user->getId(), + $user->getId(), + $user->getId(), + $user->getId(), + $checked, + $thread->getId(), + $user->getId(), + (($U = $user->getUser()) && ($A = $U->getAvatar())) + ? $U->getAvatar()->getImageTag(24) : '', + Format::htmlchars($user->getName()), + $user->getEmail()); + + if ($thread->object_type == 'T') { + echo sprintf('<td> + <select name="recipientType[]"> + <option value="Cc" %s>Cc</option> + <option value="Bcc" %s>Bcc</option> + </select> + </td>', $cc, $bcc); + } + + echo sprintf('<td width="10"> + <input type="hidden" name="del[]" id="d%d" value=""> + <a class="remove" href="#d%d"> + <i class="icon-trash icon-fixed-width"></i> + </a> + </td> + <td width="30"> </td> + </tr>',$user->getId(), $user->getId()); } ?> + <td> + <div><a class="collaborator" id="addcollaborator" + href="#thread/<?php echo $thread->getId(); ?>/add-collaborator" + ><i class="icon-plus-sign"></i> <?php echo __('Add Collaborator'); ?></a></div> + </td> </table> <hr style="margin-top:1em"/> - <div><a class="collaborator" - href="#thread/<?php echo $thread->getId(); ?>/add-collaborator" - ><i class="icon-plus-sign"></i> <?php echo __('Add New Collaborator'); ?></a></div> <div id="savewarning" style="display:none; padding-top:2px;"><p id="msg_warning"><?php echo __('You have made changes that you need to save.'); ?></p></div> <p class="full-width"> @@ -136,4 +163,8 @@ $(function() { }); }); + +function refreshAndClose(tid, type) { + window.location.href = type + '.php?id=' + tid; +} </script> diff --git a/include/staff/templates/task-view.tmpl.php b/include/staff/templates/task-view.tmpl.php index b2dff1253..84cbcdea7 100644 --- a/include/staff/templates/task-view.tmpl.php +++ b/include/staff/templates/task-view.tmpl.php @@ -619,7 +619,6 @@ else <?php echo $reply_attachments_form->getMedia(); ?> - <script type="text/javascript"> $(function() { $(document).off('.tasks-content'); diff --git a/include/staff/templates/thread-email-recipients.tmpl.php b/include/staff/templates/thread-email-recipients.tmpl.php new file mode 100644 index 000000000..3bae51c02 --- /dev/null +++ b/include/staff/templates/thread-email-recipients.tmpl.php @@ -0,0 +1,30 @@ +<?php +if (!$_REQUEST['mode']) { ?> +<h3 class="drag-handle"><?php echo __('Email Recipients'); ?></h3> +<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b> +<hr/> +<?php +} ?> +<p> +<table> +<?php +$recipients = Format::htmlchars($recipients); + foreach ($recipients as $k => $v) { + echo sprintf('<tr><td nowrap width="5" valign="top"><b>%s</b>:</td><td>%s</td></tr>', + ucfirst($k), + implode('<br>', $v) + ); + } + ?> +</table> +<?php +if (!$_REQUEST['mode']) {?> +<hr> +<p class="full-width"> + <span class="buttons pull-right"> + <input type="button" name="cancel" class="close" + value="<?php echo __('Close'); ?>"> + </span> +</p> +<?php +} ?> diff --git a/include/staff/templates/thread-entry.tmpl.php b/include/staff/templates/thread-entry.tmpl.php index a904670f2..d36d2b7c9 100644 --- a/include/staff/templates/thread-entry.tmpl.php +++ b/include/staff/templates/thread-entry.tmpl.php @@ -7,12 +7,17 @@ if ($thisstaff && !strcasecmp($thisstaff->datetime_format, 'relative')) { }; } -$entryTypes = array('M'=>'message', 'R'=>'response', 'N'=>'note'); +$entryTypes = array('M'=>'message', 'R'=>'response', 'N'=>'note','B' => 'bccmessage'); $user = $entry->getUser() ?: $entry->getStaff(); $name = $user ? $user->getName() : $entry->poster; $avatar = ''; if ($user && $cfg->isAvatarsEnabled()) $avatar = $user->getAvatar(); + +if ($entry->flags & ThreadEntry::FLAG_COLLABORATOR && $entry->type == 'N') { + $entryTypes[$entry->type] = 'bccmessage'; + $entry->type = 'B'; +} ?> <div class="thread-entry <?php echo $entry->isSystem() ? 'system' : $entryTypes[$entry->type]; ?> <?php if ($avatar) echo 'avatar'; ?>"> @@ -53,9 +58,12 @@ if ($user && $cfg->isAvatarsEnabled()) if ($entry->flags & ThreadEntry::FLAG_RESENT) { ?> <span class="label label-bare"><?php echo __('Resent'); ?></span> <?php } - if ($entry->flags & ThreadEntry::FLAG_COLLABORATOR) { ?> - <span class="label label-bare"><?php echo __('Collaborator'); ?></span> -<?php } ?> + if ($entry->flags & ThreadEntry::FLAG_COLLABORATOR && $entry->type == 'B') { ?> + <span class="label label-bare"><?php echo __('Bcc Collaborator'); ?></span> +<?php } + if ($entry->flags & ThreadEntry::FLAG_COLLABORATOR && $entry->type == 'M') { ?> + <span class="label label-bare"><?php echo __('Cc Collaborator'); ?></span> + <?php } ?> </span> </div> <?php diff --git a/include/staff/ticket-open.inc.php b/include/staff/ticket-open.inc.php index 78912dfb5..1056cdab8 100644 --- a/include/staff/ticket-open.inc.php +++ b/include/staff/ticket-open.inc.php @@ -45,7 +45,7 @@ if ($_POST) <tbody> <tr> <th colspan="2"> - <em><strong><?php echo __('User Information'); ?></strong>: </em> + <em><strong><?php echo __('Recipient Information'); ?></strong>: </em> <div class="error"><?php echo $errors['user']; ?></div> </th> </tr> @@ -116,6 +116,64 @@ if ($_POST) </tr> <?php } ?> + <tr> + <td> + <table border="0"> + <tr class="no_border"> + <td width="120"> + <label><strong><?php echo __('Collaborators'); ?>:</strong></label> + </td> + <td> + <input type='checkbox' value='1' name="emailcollab" + id="emailcollab" + <?php echo ((!$info['emailcollab'] && !$errors) || isset($info['emailcollab']))?'checked="checked"':''; ?> + + > + <?php + ?> + </td> + </tr> + <tr class="no_border" id="ccRow"> + <td width="160"><?php echo __('Cc'); ?>:</td> + <td> + <select name="ccs[]" id="cc_users_open" multiple="multiple" + data-placeholder="<?php echo __('Select Contacts'); ?>"> + <option value=""></option> + <?php + $users = User::objects(); + foreach ($users as $u) { + if($user && $u->id != $user->getId()) { + ?> + <option value="<?php echo $u->id; ?>" + ><?php echo $u->getName(); ?> + </option> + <?php } } ?> + </select> + <br/><span class="error"><?php echo $errors['ccs']; ?></span> + </td> + </tr> + <tr class="no_border" id="bccRow"> + <td width="160"><?php echo __('Bcc'); ?>:</td> + <td> + <select name="bccs[]" id="bcc_users_open" multiple="multiple" + data-placeholder="<?php echo __('Select Contacts'); ?>"> + <option value=""></option> + <?php + $users = User::objects(); + foreach ($users as $u) { + if($user && $u->id != $user->getId()) { + ?> + <option value="<?php echo $u->id; ?>" + ><?php echo $u->getName(); ?> + </option> + <?php } } ?> + </select> + <br/><span class="error"><?php echo $errors['ccs']; ?></span> + </td> + </tr> + </table> + </td> + </tr> </tbody> <tbody> <tr> @@ -449,5 +507,47 @@ $(function() { <?php } ?> }); -</script> +$(function() { + $('a#editorg').click( function(e) { + e.preventDefault(); + $('div#org-profile').hide(); + $('div#org-form').fadeIn(); + return false; + }); + + $(document).on('click', 'form.org input.cancel', function (e) { + e.preventDefault(); + $('div#org-form').hide(); + $('div#org-profile').fadeIn(); + return false; + }); + $("#cc_users_open").select2({width: '300px'}); + $("#bcc_users_open").select2({width: '300px'}); +}); + +$(document).ready(function () { + $('#emailcollab').on('change', function(){ + var value = $("#cc_users_open").val(); + if ($(this).prop('checked')) { + $('#ccRow').show(); + $('#bccRow').show(); + } + else { + $('#ccRow').hide(); + $('#bccRow').hide(); + } + }); +}); + +$("form").submit(function(event) { + var value = $("#emailcollab").val(); + if ($("#emailcollab").prop('checked')) { + //do nothing + } + else { + $("#cc_users_open").val(null).change(); + $("#bcc_users_open").val(null).change(); + } +}); +</script> diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index 675e99642..b216b3f8b 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -179,6 +179,19 @@ if($ticket->isOverdue()) <?php } ?> + <li> + + <?php + $recipients = __(' Manage Collaborators'); + + echo sprintf('<a class="collaborators manage-collaborators" + href="#thread/%d/collaborators"><i class="icon-group"></i>%s</a>', + $ticket->getThreadId(), + $recipients); + ?> + </li> + + <?php if ($thisstaff->hasPerm(Email::PERM_BANLIST)) { if(!$emailBanned) {?> <li><a class="confirm-action" id="ticket-banemail" @@ -301,6 +314,21 @@ if($ticket->isOverdue()) <?php } ?> </ul> </div> + <?php + if ($role->hasPerm(TicketModel::PERM_EDIT)) { + $numCollaborators = $ticket->getThread()->getNumCollaborators(); + if ($ticket->getThread()->getNumCollaborators()) + $recipients = sprintf(__('%d'), + $numCollaborators); + else + $recipients = 0; + + echo sprintf('<span><a class="collaborators preview" + href="#thread/%d/collaborators"><span id="t%d-recipients"><i class="icon-group"></i> (%s)</span></a></span>', + $ticket->getThreadId(), + $ticket->getThreadId(), + $recipients); + }?> <?php } # end if ($user) ?> </td> </tr> @@ -554,6 +582,30 @@ if ($errors['err'] && isset($_POST['a'])) { <?php }?> <tbody id="to_sec"> + <tr> + <td width="120"> + <label><strong><?php echo __('From'); ?>:</strong></label> + </td> + <td> + <?php + # XXX: Add user-to-name and user-to-email HTML ID#s + $addresses = Email::getAddresses(); + ?> + <select id="from_name" name="from_name"> + <?php + $sql=' SELECT email_id, email, name, smtp_host ' + .' FROM '.EMAIL_TABLE.' WHERE smtp_active = 1'; + if(($res=db_query($sql)) && db_num_rows($res)) { + while (list($id, $email, $name, $host) = db_fetch_row($res)){ + $email=$name?"$name <$email>":$email; + ?> + <option value="<?php echo $id; ?>"<?php echo ($dept->getEmail()->email_id==$id)?'selected="selected"':''; ?>><?php echo $email; ?></option> + <?php + } + } ?> + </select> + </td> + </tr> <tr> <td width="120"> <label><strong><?php echo __('To'); ?>:</strong></label> @@ -590,20 +642,62 @@ if ($errors['err'] && isset($_POST['a'])) { style="display:<?php echo $ticket->getThread()->getNumCollaborators() ? 'inline-block': 'none'; ?>;" > <?php - $recipients = __('Add Recipients'); - if ($ticket->getThread()->getNumCollaborators()) - $recipients = sprintf(__('Recipients (%d of %d)'), - $ticket->getThread()->getNumActiveCollaborators(), - $ticket->getThread()->getNumCollaborators()); - - echo sprintf('<span><a class="collaborators preview" - href="#thread/%d/collaborators"><span id="t%d-recipients">%s</span></a></span>', - $ticket->getThreadId(), - $ticket->getThreadId(), - $recipients); ?> </td> </tr> + <?php $collaborators = $ticket->getThread()->getCollaborators(); + $cc_cids = array(); + $bcc_cids = array(); + foreach ($collaborators as $c) { + if ($c->flags & Collaborator::FLAG_CC && $c->flags & Collaborator::FLAG_ACTIVE) + $cc_cids[] = $c->user_id; + elseif (!($c->flags & Collaborator::FLAG_CC) && $c->flags & Collaborator::FLAG_ACTIVE) { + $bcc_cids[] = $c->user_id; + } + } + ?> + <tr> + <td width="160"><b><?php echo __('Cc'); ?>:</b></td> + <td> + <select name="ccs[]" id="cc_users" multiple="multiple" + data-placeholder="<?php echo __('Select Contacts'); ?>"> + <option value=""></option> + <option value="NEW">— <?php echo __('Add New');?> —</option> + <?php + $users = User::objects(); + foreach ($users as $u) { + if($u->id != $ticket->user_id && !in_array($u->getId(), $bcc_cids)) { + ?> + <option value="<?php echo $u->id; ?>" <?php + if (in_array($u->getId(), $cc_cids)) + echo 'selected="selected"'; ?>><?php echo $u->getName(); ?> + </option> + <?php } } ?> + </select> + <br/><span class="error"><?php echo $errors['ccs']; ?></span> + </td> + </tr> + <tr> + <td width="160"><b><?php echo __('Bcc'); ?>:</b></td> + <td> + <select name="bccs[]" id="bcc_users" multiple="multiple" + data-placeholder="<?php echo __('Select Contacts'); ?>"> + <option value=""></option> + <option value="NEW">— <?php echo __('Add New');?> —</option> + <?php + $users = User::objects(); + foreach ($users as $u) { + if($u->id != $ticket->user_id && !in_array($u->getId(), $cc_cids)) { + ?> + <option value="<?php echo $u->id; ?>" <?php + if (in_array($u->getId(), $bcc_cids)) + echo 'selected="selected"'; ?>><?php echo $u->getName(); ?> + </option> + <?php } } ?> + </select> + <br/><span class="error"><?php echo $errors['bccs']; ?></span> + </td> + </tr> </tbody> <?php } ?> @@ -967,4 +1061,78 @@ $(function() { }); }); + +$(function() { + $("#cc_users").select2({width: '350px'}); + $("#bcc_users").select2({width: '350px'}); +}); + +$(function() { + $('#cc_users').on("select2:select", function(e) { + var el = $(this); + var tid = <?php echo $ticket->getThreadId(); ?>; + + if(el.val().includes("NEW")) { + $("li[title='— Add New —']").remove(); + var url = 'ajax.php/thread/' + tid + '/add-collaborator' ; + $.userLookup(url, function(user) { + e.preventDefault(); + if($('.dialog#confirm-action').length) { + $('.dialog#confirm-action #action').val('addcc'); + $('#confirm-form').append('<input type=hidden name=user_id value='+user.id+' />'); + $('#overlay').show(); + } + }); + var arr = el.val(); + var removeStr = "NEW"; + + arr.splice($.inArray(removeStr, arr),1); + $(this).val(arr); + } + }); + + $('#bcc_users').on("select2:select", function(e) { + var el = $(this); + var tid = <?php echo $ticket->getThreadId(); ?>; + + if(el.val().includes("NEW")) { + $("li[title='— Add New —']").remove(); + var url = 'ajax.php/thread/' + tid + '/add-collaborator' ; + $.userLookup(url, function(user) { + e.preventDefault(); + if($('.dialog#confirm-action').length) { + $('.dialog#confirm-action #action').val('addbcc'); + $('#confirm-form').append('<input type=hidden name=user_id value='+user.id+' />'); + $('#overlay').show(); + } + }); + var arr = el.val(); + var removeStr = "NEW"; + + arr.splice($.inArray(removeStr, arr),1); + $(this).val(arr); + } + }); + + $('#cc_users').on("select2:unselecting", function(e) { + var confirmation = confirm(__("Are you sure you want to remove the collaborator from receiving this reply?")); + if (confirmation == false) { + $('#cc_users').on("select2:opening", function(e) { + return false; + }); + return false; + } + + }); + + $('#bcc_users').on("select2:unselecting", function(e) { + var confirmation = confirm(__("Are you sure you want to remove the collaborator from receiving this reply?")); + if (confirmation == false) { + $('#bcc_users').on("select2:opening", function(e) { + return false; + }); + return false; + } + }); +}); </script> diff --git a/include/upgrader/streams/core/98ad7d55-b2ce8ba7.patch.sql b/include/upgrader/streams/core/98ad7d55-b2ce8ba7.patch.sql new file mode 100644 index 000000000..3c10b87fd --- /dev/null +++ b/include/upgrader/streams/core/98ad7d55-b2ce8ba7.patch.sql @@ -0,0 +1,31 @@ +/** + * @version v1.11.0 + * @title Add recipients field/collaborator flags + * @signature b2ce8ba794a40ed5380d7cdf30bca233 + * + * This patch adds a new field called recipients to the thread entry table + * allowing agents to see a list of recipients for any thread entry where + * an email was involved (agent or user generated) + * + * It also adds a flags field to the thread_collaborator table which + * tracks whether a collaborator is a CC or BCC collaborator as well as + * storing whether or not the collaborator is active. As a result, we can + * remove the isactive field + */ + + ALTER TABLE `%TABLE_PREFIX%thread_entry` + ADD `recipients` text AFTER `ip_address`; + + ALTER TABLE `%TABLE_PREFIX%thread_collaborator` + ADD `flags` int(10) unsigned NOT NULL DEFAULT 1 AFTER `id`; + + UPDATE `%TABLE_PREFIX%thread_collaborator` + SET `flags` = `isactive` + 2; + + ALTER TABLE `%TABLE_PREFIX%thread_collaborator` + DROP COLUMN `isactive`; + + -- Finished with patch +UPDATE `%TABLE_PREFIX%config` + SET `value` = 'b2ce8ba794a40ed5380d7cdf30bca233' + WHERE `key` = 'schema_signature' AND `namespace` = 'core'; diff --git a/scp/css/scp.css b/scp/css/scp.css index 13d4c9643..d747a7a08 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -1310,6 +1310,10 @@ table.fixed > tr > td + td:not([width]) { width: auto; } +tr.no_border > td, td.no_border{ + border-style:hidden; +} + td.multi-line { vertical-align:top; padding-top: 0.4em; @@ -1584,6 +1588,27 @@ img.avatar { position: relative; } +.thread-entry.bccmessage .header { + background:#DDFDAC; +} + +.thread-entry.avatar.bccmessage .header:before { + top: 7px; + left: -8px; + right: initial; + border-left: none; + border-right: 8px solid #CCC; +} + +.thread-entry.avatar.bccmessage .header:after { + top: 7px; + left: -8px; + right: initial; + border-left: none; + border-right: 7px solid #DDFDAC; + margin-left: 1px; +} + .thread-entry.message .header { background:#C3D9FF; } diff --git a/scp/tasks.php b/scp/tasks.php index 327de9a26..23f83dba0 100644 --- a/scp/tasks.php +++ b/scp/tasks.php @@ -41,7 +41,6 @@ $reply_attachments_form = new SimpleForm(array( //At this stage we know the access status. we can process the post. if($_POST && !$errors): - if ($task) { //More coffee please. $errors=array(); @@ -110,6 +109,24 @@ if($_POST && !$errors): default: $errors['err']=__('Unknown action'); endswitch; + + switch(strtolower($_POST['do'])): + case 'addcc': + $errors = array(); + if (!$role->hasPerm(TicketModel::PERM_EDIT)) { + $errors['err']=__('Permission Denied. You are not allowed to add collaborators'); + } elseif (!$_POST['user_id'] || !($user=User::lookup($_POST['user_id']))) { + $errors['err'] = __('Unknown user selected'); + } elseif ($c2 = $task->addCollaborator($user, array('isactive'=>1), $errors)) { + $c2->setFlag(Collaborator::FLAG_CC, true); + $c2->save(); + $msg = sprintf(__('Collaborator %s added'), + Format::htmlchars($user->getName())); + } + else + $errors['err'] = sprintf('%s %s', __('Unable to add collaborator.'), __('Please try again!')); + break; + endswitch; } if(!$errors) $thisstaff->resetStats(); //We'll need to reflect any changes just made! diff --git a/scp/tickets.php b/scp/tickets.php index e2fe93c79..a79354e61 100644 --- a/scp/tickets.php +++ b/scp/tickets.php @@ -366,6 +366,34 @@ if($_POST && !$errors): $errors['err'] = sprintf('%s %s', __('Unable to change ticket ownership.'), __('Please try again!')); } break; + case 'addcc': + if (!$role->hasPerm(TicketModel::PERM_EDIT)) { + $errors['err']=__('Permission Denied. You are not allowed to add collaborators'); + } elseif (!$_POST['user_id'] || !($user=User::lookup($_POST['user_id']))) { + $errors['err'] = __('Unknown user selected'); + } elseif ($c2 = $ticket->addCollaborator($user, array('isactive'=>1), $errors)) { + $c2->setFlag(Collaborator::FLAG_CC, true); + $c2->save(); + $msg = sprintf(__('Collaborator %s added'), + Format::htmlchars($user->getName())); + } + else { + $errors['err'] = sprintf('%s %s', __('Unable to add collaborator.'), __('Please try again!')); + } + break; + case 'addbcc': + if (!$role->hasPerm(TicketModel::PERM_EDIT)) { + $errors['err']=__('Permission Denied. You are not allowed to add collaborators'); + } elseif (!$_POST['user_id'] || !($user=User::lookup($_POST['user_id']))) { + $errors['err'] = __('Unknown user selected'); + } elseif ($c2 = $ticket->addCollaborator($user, array('isactive'=>1), $errors)) { + $msg = sprintf(__('Collaborator %s added'), + Format::htmlchars($user->getName())); + } + else { + $errors['err'] = sprintf('%s %s', __('Unable to add collaborator.'), __('Please try again!')); + } + break; default: $errors['err']=__('You must select action to perform'); endswitch; diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql index 1893c482e..bd9651c5f 100644 --- a/setup/inc/streams/core/install-mysql.sql +++ b/setup/inc/streams/core/install-mysql.sql @@ -637,6 +637,7 @@ CREATE TABLE `%TABLE_PREFIX%thread_entry` ( `body` text NOT NULL, `format` varchar(16) NOT NULL default 'html', `ip_address` varchar(64) NOT NULL default '', + `recipients` text, `created` datetime NOT NULL, `updated` datetime NOT NULL, PRIMARY KEY (`id`), @@ -761,7 +762,7 @@ CREATE TABLE `%TABLE_PREFIX%ticket_priority` ( CREATE TABLE `%TABLE_PREFIX%thread_collaborator` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `isactive` tinyint(1) NOT NULL DEFAULT '1', + `flags` int(10) unsigned NOT NULL DEFAULT '1', `thread_id` int(11) unsigned NOT NULL DEFAULT '0', `user_id` int(11) unsigned NOT NULL DEFAULT '0', -- M => (message) clients, N => (note) 3rd-Party, R => (reply) external authority -- GitLab