diff --git a/include/ajax.thread.php b/include/ajax.thread.php index 7e72aef126e2992dcba539dfb8da011a4244f0d6..288634bb83c3f4a248f197b10bb4bd82482a6b47 100644 --- a/include/ajax.thread.php +++ b/include/ajax.thread.php @@ -223,8 +223,15 @@ class ThreadAjaxAPI extends AjaxController { $errors = $info = array(); if ($thread->updateCollaborators($_POST, $errors)) + $users = array(); + foreach ($thread->getCollaborators() as $c) + $users[] = array('id' => $c->getUserId(), + 'name' => $c->getName(), + 'email' => $c->getEmail()); + Http::response(201, $this->json_encode(array( 'id' => $thread->getId(), + 'users' => $users, 'text' => sprintf('(%d)', $thread->getNumCollaborators()) ) diff --git a/include/class.client.php b/include/class.client.php index 36fa13746ca5cfbba82da2577214718e8418eed6..1e82ebe411a214ecbc1e8f94dd33f2e5c120d836 100644 --- a/include/class.client.php +++ b/include/class.client.php @@ -82,6 +82,9 @@ implements EmailContact, ITicketUser, TemplateVariable { function getId() { return ($this->user) ? $this->user->getId() : null; } function getEmail() { return ($this->user) ? $this->user->getEmail() : null; } + function getName() { + return ($this->user) ? $this->user->getName() : null; + } static function lookupByToken($token) { @@ -158,6 +161,11 @@ class TicketOwner extends TicketUser { $this->ticket = $ticket; } + function __toString() { + return (string) $this->getName(); + } + + function getTicket() { return $this->ticket; } @@ -470,12 +478,6 @@ class ClientAccount extends UserAccount { } } -// Used by the email system -interface EmailContact { - // function getId() - // function getName() - // function getEmail() -} interface ITicketUser { /* PHP 5.3 < 5.3.8 will crash with some abstract inheritance issue diff --git a/include/class.collaborator.php b/include/class.collaborator.php index 94e97fee65f55a37d9cff4f27707fee0934a742a..dc35a436f06c3ee2f5b2e0c1c9cb5847be864970 100644 --- a/include/class.collaborator.php +++ b/include/class.collaborator.php @@ -168,6 +168,8 @@ implements EmailContact, ITicketUser { static function create($vars=false) { $inst = new static($vars); + $inst->setFlag(Collaborator::FLAG_ACTIVE, true); + $inst->setFlag(Collaborator::FLAG_CC, true); $inst->created = SqlFunction::NOW(); return $inst; } diff --git a/include/class.email.php b/include/class.email.php index 41bb55a1a3ca3f84e1fb3b28357558a9e45f1c5e..4c338f50e076c16ed465987cf5d91e7e6ca406bd 100644 --- a/include/class.email.php +++ b/include/class.email.php @@ -445,11 +445,14 @@ class Email extends VerySimpleModel { return self::$perms; } - static function getAddresses($options=array()) { + static function getAddresses($options=array(), $flat=true) { $objects = static::objects(); if ($options['smtp']) $objects = $objects->filter(array('smtp_active'=>true)); + if (!$flat) + return $objects; + $addresses = array(); foreach ($objects->values_flat('email_id', 'email') as $row) { list($id, $email) = $row; diff --git a/include/class.mailer.php b/include/class.mailer.php index ed1ef55df3f9c1d779ee72f3172b5d3b5a536c6e..017d7fbb0ec99267cf1e51920a4e2dd62a88e13d 100644 --- a/include/class.mailer.php +++ b/include/class.mailer.php @@ -165,8 +165,11 @@ class Mailer { case $recipient instanceof Collaborator: $utype = 'C'; break; + case $recipient instanceof MailingList: + $utype = 'M'; + break; default: - $utype = $options['utype'] ?: '?'; + $utype = $options['utype'] ?: is_array($recipient) ? 'M' : '?'; } @@ -209,6 +212,7 @@ class Mailer { * 'U' - TicketOwner * 'S' - Staff * 'C' - Collborator + * 'M' - Multiple * '?' - Something else */ static function decodeMessageId($mid) { @@ -294,36 +298,17 @@ class Mailer { 0, 6); } - function send($recipient, $subject, $message, $options=null, $collabs=array()) { + function send($recipients, $subject, $message, $options=null) { global $ost, $cfg; //Get the goodies require_once (PEAR_DIR.'Mail.php'); // PEAR Mail package require_once (PEAR_DIR.'Mail/mime.php'); // PEAR Mail_Mime packge - $messageId = $this->getMessageId($recipient, $options); - - if (is_object($recipient) && is_callable(array($recipient, 'getEmail'))) { - // Add personal name if available - if (is_callable(array($recipient, 'getName'))) { - $to = sprintf('"%s" <%s>', - $recipient->getName()->getOriginal(), $recipient->getEmail() - ); - } - else { - $to = $recipient->getEmail(); - } - } else { - $to = $recipient; - } - - //do some cleanup - $to = preg_replace("/(\r\n|\r|\n)/s",'', trim($to)); + $messageId = $this->getMessageId($recipients, $options); $subject = preg_replace("/(\r\n|\r|\n)/s",'', trim($subject)); - $headers = array ( 'From' => $this->getFromAddress($options), - 'To' => $to, 'Subject' => $subject, 'Date'=> date('D, d M Y H:i:s O'), 'Message-ID' => "<{$messageId}>", @@ -345,12 +330,12 @@ class Mailer { $entry = null; switch (true) { - case $recipient instanceof TicketOwner: - case $recipient instanceof Collaborator: + case $recipients instanceof TicketOwner: + case $recipients instanceof Collaborator: $entry = $thread->getLastEmailMessage(array( - 'user_id' => $recipient->getUserId())); + 'user_id' => $recipients->getUserId())); break; - case $recipient instanceof Staff: + case $recipients instanceof Staff: //XXX: is it necessary ?? break; } @@ -413,13 +398,35 @@ class Mailer { // Use general failsafe default initially $eol = "\n"; - // MAIL_EOL setting can be defined in `ost-config.php` if (defined('MAIL_EOL') && is_string(MAIL_EOL)) { $eol = MAIL_EOL; } + $mime = new Mail_mime($eol); + // Add recipients + if (!is_array($recipients) && (!$recipients instanceof MailingList)) + $recipients = array($recipients); + foreach ($recipients as $recipient) { + switch (true) { + case $recipient instanceof TicketOwner: + case $recipient instanceof Staff: + $mime->addTo(sprintf('%s <%s>', + $recipient->getName(), + $recipient->getEmail())); + break; + case $recipient instanceof Collaborator: + $mime->addCc(sprintf('%s <%s>', + $recipient->getName(), + $recipient->getEmail())); + break; + default: + // Assuming email address. + $mime->addTo($recipient); + } + } + // Add in extra attachments, if any from template variables if ($message instanceof TextWithExtras && ($files = $message->getFiles()) @@ -508,19 +515,6 @@ 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', @@ -534,7 +528,8 @@ class Mailer { $body = $mime->get($encodings); //encode the headers. $headers = $mime->headers($headers, true); - + $to = implode(',', array_filter(array($headers['To'], $headers['Cc'], + $headers['Bcc']))); // Cache smtp connections made during this request static $smtp_connections = array(); if(($smtp=$this->getSMTPInfo())) { //Send via SMTP @@ -577,8 +572,6 @@ class Mailer { if ($this->getEmail()) $args = array('-f '.$this->getEmail()->getEmail()); $mail = mail::factory('mail', $args); - // Ensure the To: header is properly encoded. - $to = $headers['To']; $result = $mail->send($to, $headers, $body); if(!PEAR::isError($result)) return $messageId; diff --git a/include/class.thread.php b/include/class.thread.php index a28c5c369f21df69c82556e446a64cd6edcda384..177c608a0f66f6a9461ee00e3a01ce8f4815ffa3 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -1468,11 +1468,11 @@ implements TemplateVariable { function setReplyFlag($entry, $replyType) { switch ($replyType) { - case 'reply-all': + case 'all': return $entry->flags |= ThreadEntry::FLAG_REPLY_ALL; break; - case 'reply-user': + case 'user': return $entry->flags |= ThreadEntry::FLAG_REPLY_USER; break; } @@ -1522,7 +1522,7 @@ implements TemplateVariable { $ticketUser = Ticket::objects()->filter(array('ticket_id'=>$ticket[0]))->values_flat('user_id')->first(); //User - if ($ticketUser && Ticket::checkReply('user', $vars['emailreply'])) { + if ($ticketUser && strcasecmp('none', $vars['reply-to'])) { $uEmail = UserEmailModel::objects()->filter(array('user_id'=>$ticketUser[0]))->values_flat('address')->first(); $u = array(); $u[$ticketUser[0]] = $uEmail[0]; @@ -1533,15 +1533,15 @@ implements TemplateVariable { $entry->flags |= ThreadEntry::FLAG_COLLABORATOR; //add reply type flag - self::setReplyFlag($entry, $vars['emailreply']); + self::setReplyFlag($entry, $vars['reply-to']); //Cc collaborators - if ($vars['ccs'] && Ticket::checkReply('cc', $vars['emailreply'])) { + if ($vars['ccs'] && !strcasecmp('all', $vars['reply-to'])) { $cc = Collaborator::getCollabList($vars['ccs']); $recipients['cc'] = $cc; } - if ($vars['emailreply'] != '0' && $recipients) + if ($vars['reply-to'] != 'none' && $recipients) $entry->recipients = json_encode($recipients); if ($entry->format == 'html') diff --git a/include/class.ticket.php b/include/class.ticket.php index 71e890add94467448a316b9f23728a3ec80c2b34..f9646b39df8096adb8c07a48c1b8de681c143cb5 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -841,20 +841,36 @@ implements RestrictedAccess, Threadable, Searchable { return $entries; } - //UserList of recipients (owner + collaborators) + // MailingList of recipients (owner + active collaborators) function getRecipients() { - $list = new UserList(); - $list->add($this->getOwner()); - if ($collabs = $this->getThread()->getActiveCollaborators()) { - foreach ($collabs as $c) { - $list->add($c); - } - } - $this->recipients = $list; - + if (!isset($this->recipients)) { + $list = new MailingList(); + $list->add($this->getOwner()); + if ($collabs = $this->getActiveCollaborators()) { + foreach ($collabs as $c) + $list->add($c); + } + $this->recipients = $list; + } return $this->recipients; } + function getCollaborators() { + return $this->getThread()->getCollaborators(); + } + + function getNumCollaborators() { + return $this->getThread()->getNumCollaborators(); + } + + function getActiveCollaborators() { + return $this->getThread()->getActiveCollaborators(); + } + + function getNumActiveCollaborators() { + return $this->getThread()->getNumActiveCollaborators(); + } + function getAssignmentForm($source=null, $options=array()) { $prompt = $assignee = ''; @@ -1091,6 +1107,25 @@ implements RestrictedAccess, Threadable, Searchable { return $c; } + function addCollaborators($users, $vars, &$errors, $event=true) { + + if (!$users || !is_array($users)) + return null; + + $collabs = $this->getCollaborators(); + $new = array(); + foreach ($users as $user) { + if (!($user instanceof User) + && !($user = User::lookup($user))) + continue; + if ($collabs->findFirst(array('user_id' => $user->getId()))) + continue; + if ($c=$this->addCollaborator($user, $vars, $errors, $event)) + $new[] = $c; + } + return $new; + } + //XXX: Ugly for now function updateCollaborators($vars, &$errors) { global $thisstaff; @@ -1382,17 +1417,6 @@ implements RestrictedAccess, Threadable, Searchable { return false; } - function checkReply($userType, $replyType) { - if ($userType == 'cc' && $replyType == 'reply-all') - return true; - - if ($userType == 'user' && ($replyType == 'reply-all' || $replyType == 'reply-user')) - return true; - - return false; - } - - function setAnsweredState($isanswered) { $this->isanswered = $isanswered; return $this->save(); @@ -1597,6 +1621,7 @@ implements RestrictedAccess, Threadable, Searchable { $poster = User::lookup($entry->user_id); $posterEmail = $poster->getEmail()->address; + $recipients = array(); if($vars['ccs']) { foreach ($vars['ccs'] as $cc) { $collab = Collaborator::getIdByUserId($cc, $this->getThread()->getId()); @@ -2175,8 +2200,6 @@ implements RestrictedAccess, Threadable, Searchable { function replaceVars($input, $vars = array()) { global $ost; - $recipients = $this->getRecipients(); - $vars = array_merge($vars, array('ticket' => $this)); return $ost->replaceTemplateVariables($input, $vars); } @@ -2862,20 +2885,16 @@ 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']); + // Add new collabs if any. + $collabs = $this->getCollaborators(); 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(), $errors))) - $c2->setCc(); - } + foreach ($vars['ccs'] as $uid) { + if ($collabs->findFirst(array('user_id' => $uid))) + continue; + + if ($user=User::lookup($uid)) + $this->addCollaborator($user, array(), $errors); + } } if (!$vars['poster'] && $thisstaff) @@ -2917,7 +2936,9 @@ implements RestrictedAccess, Threadable, Searchable { return $response; //allow agent to send from different dept email - $vars['from_name'] ? $email = Email::lookup($vars['from_name']) : $email = $dept->getEmail(); + if (!$vars['from_email_id'] + || !($email = Email::lookup($vars['from_email_id']))) + $email = $dept->getEmail(); $options = array('thread'=>$response); $signature = $from_name = ''; @@ -2951,26 +2972,32 @@ implements RestrictedAccess, Threadable, Searchable { 'poster' => $thisstaff ); - $user = $this->getOwner(); - if (($email=$email) + if ($email && ($tpl = $dept->getTemplate()) && ($msg=$tpl->getReplyMsgTemplate())) { $msg = $this->replaceVars($msg->asArray(), - $variables + array('recipient' => $user) + $variables + array('recipient' => $this->getOwner()) ); - $attachments = $cfg->emailAttachments() ? $response->getAttachments() : array(); - //Cc collaborators - $collabsCc = array(); - if ($vars['ccs']) { - $collabsCc[] = Collaborator::getCollabList($vars['ccs']); - $collabsCc['cc'] = $collabsCc[0]; + // Attachments + $attachments = $cfg->emailAttachments() ? + $response->getAttachments() : array(); + + // Get active recipients + $recipients = new MailingList(); + $recipients->add($this->getOwner()); + if (!strcasecmp('all', $vars['reply-to']) + && ($collabs = $this->getActiveCollaborators())){ + foreach ($collabs as $c) + if ($vars['ccs'] && in_array($c->getUserId(), + $vars['ccs'])) + $recipients->add($c); } - $email->send($user, $msg['subj'], $msg['body'], $attachments, - $options, $collabsCc); - + //Send email to recepients + $email->send($recipients, $msg['subj'], $msg['body'], + $attachments, $options); } return $response; @@ -4043,33 +4070,28 @@ implements RestrictedAccess, Threadable, Searchable { if (!($ticket=self::create($create_vars, $errors, 'staff', false))) return false; - $collabsCc = 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 (isset($vars['ccs'])) + $ticket->addCollaborators($vars['ccs'], array(), $errors); - if (($c2=$ticket->getThread()->addCollaborator($ccuser,array(), $errors))) - $c2->setCc(); - } - } - $collabsCc['cc'] = $collabsCc; - } + if (strcasecmp('user', $vars['reply-to'])) + $recipients = $ticket->getRecipients(); + else + $recipients = $ticket->getOwner(); $vars['msgId']=$ticket->getLastMsgId(); // Effective role for the department $role = $ticket->getRole($thisstaff); + $alert = strcasecmp('none', $vars['reply-to']); + // post response - if any $response = null; if($vars['response'] && $role->hasPerm(Ticket::PERM_REPLY)) { $vars['response'] = $ticket->replaceVars($vars['response']); // $vars['cannedatachments'] contains the attachments placed on // the response form. - $response = $ticket->postReply($vars, $errors, is_null($vars['emailreply']) ?: $vars['emailreply'] === "0"); + $response = $ticket->postReply($vars, $errors, $alert && !$cfg->notifyONNewStaffTicket()); } // Not assigned...save optional note if any @@ -4081,7 +4103,7 @@ implements RestrictedAccess, Threadable, Searchable { } if (!$cfg->notifyONNewStaffTicket() - || !isset($vars['emailreply']) + || !$alert || !($dept=$ticket->getDept()) ) { return $ticket; //No alerts. @@ -4122,13 +4144,8 @@ implements RestrictedAccess, Threadable, Searchable { ); //ticket created on user's behalf - if (Ticket::checkReply('cc', $vars['emailreply'])) { - $email->send($ticket->getOwner(), $msg['subj'], $msg['body'], $attachments, - $options, $collabsCc); - } - elseif (Ticket::checkReply('user', $vars['emailreply'])) - $email->send($ticket->getOwner(), $msg['subj'], $msg['body'], $attachments, - $options); + $email->send($recipients, $msg['subj'], $msg['body'], $attachments, + $options); } return $ticket; } diff --git a/include/class.user.php b/include/class.user.php index 71a5289a83a14540662aa159f391c6423b7ec359..4681b3fd1a8aa596bead9462a87a2cfcd2e2a1dc 100644 --- a/include/class.user.php +++ b/include/class.user.php @@ -210,9 +210,12 @@ class UserCdata extends VerySimpleModel { class User extends UserModel implements TemplateVariable, Searchable { + var $_email; var $_entries; var $_forms; + + static function fromVars($vars, $create=true, $update=false) { // Try and lookup by email address $user = static::lookupByEmail($vars['email']); @@ -282,7 +285,13 @@ implements TemplateVariable, Searchable { } function getEmail() { - return new EmailAddress($this->default_email->address); + + if (!isset($this->_email)) + $this->_email = new EmailAddress(sprintf('"%s" <%s>', + $this->getName(), + $this->default_email->address)); + + return $this->_email; } function getAvatar($size=null) { @@ -658,36 +667,72 @@ implements TemplateVariable, Searchable { class EmailAddress implements TemplateVariable { + var $email; var $address; + protected $_info; function __construct($address) { - $this->address = $address; + $this->_info = self::parse($address); + $this->email = sprintf('%s@%s', + $this->getMailbox(), + $this->getDomain()); + + if ($this->getName()) + $this->address = sprintf('%s <%s>', + $this->getName(), + $this->email); } function __toString() { - return (string) $this->address; + return (string) $this->email; } function getVar($what) { - require_once PEAR_DIR . 'Mail/RFC822.php'; - require_once PEAR_DIR . 'PEAR.php'; - if (!($mails = Mail_RFC822::parseAddressList($this->address)) || PEAR::isError($mails)) - return ''; - if (count($mails) > 1) + if (!$this->_info) return ''; - $info = $mails[0]; switch ($what) { + case 'host': case 'domain': - return $info->host; + return $this->_info->host; case 'personal': - return trim($info->personal, '"'); + return trim($this->_info->personal, '"'); case 'mailbox': - return $info->mailbox; + return $this->_info->mailbox; } } + function getAddress() { + return $this->address ?: $this->email; + } + + function getHost() { + return $this->getVar('host'); + } + + function getDomain() { + return $this->getHost(); + } + + function getName() { + return $this->getVar('personal'); + } + + function getMailbox() { + return $this->getVar('mailbox'); + } + + // Parse and email adddress (RFC822) into it's parts. + // @address - one address is expected + static function parse($address) { + require_once PEAR_DIR . 'Mail/RFC822.php'; + require_once PEAR_DIR . 'PEAR.php'; + if (($parts = Mail_RFC822::parseAddressList($address)) + && !PEAR::isError($parts)) + return current($parts); + } + static function getVarScope() { return array( 'domain' => __('Domain'), @@ -1304,51 +1349,17 @@ class UserAccountStatus { } } - /* * Generic user list. */ -class UserList extends ListObject -implements TemplateVariable { +class UserList extends MailingList { - function __toString() { - return $this->getNames(); - } + function add($user) { + if (!$user instanceof ITicketUser) + throw new InvalidArgumentException('User expected'); - function getNames() { - $list = array(); - foreach($this->storage as $user) { - if (is_object($user)) - $list [] = $user->getName(); - } - return $list ? implode(', ', $list) : ''; - } - - function getFull() { - $list = array(); - foreach($this->storage as $user) { - if (is_object($user)) - $list[] = sprintf("%s <%s>", $user->getName(), $user->getEmail()); - } - - return $list ? implode(', ', $list) : ''; - } - - function getEmails() { - $list = array(); - foreach($this->storage as $user) { - if (is_object($user)) - $list[] = $user->getEmail(); - } - return $list ? implode(', ', $list) : ''; - } - - static function getVarScope() { - return array( - 'names' => __('List of names'), - 'emails' => __('List of email addresses'), - 'full' => __('List of names and email addresses'), - ); + return parent::add($user); } } + ?> diff --git a/include/class.util.php b/include/class.util.php index a56f23b2ce70c43f95c3b63c3d90987ab731331b..52035bcb2287a0e204745c504bb8f5c62e2b7f28 100644 --- a/include/class.util.php +++ b/include/class.util.php @@ -1,5 +1,14 @@ <?php +require_once INCLUDE_DIR . 'class.variable.php'; + +// Used by the email system +interface EmailContact { + function getId(); + function getName(); + function getEmail(); +} + abstract class BaseList implements IteratorAggregate, Countable { protected $storage = array(); @@ -177,3 +186,54 @@ implements ArrayAccess, Serializable { $this->storage = unserialize($what); } } + +class MailingList extends ListObject +implements TemplateVariable { + + function add($contact) { + if (!$contact instanceof EmailContact) + throw new InvalidArgumentException('Email Contact expected'); + + return parent::add($contact); + } + + function __toString() { + return $this->getNames(); + } + + function getNames() { + $list = array(); + foreach($this->storage as $user) { + if (is_object($user)) + $list [] = $user->getName(); + } + return $list ? implode(', ', $list) : ''; + } + + function getFull() { + $list = array(); + foreach($this->storage as $user) { + if (is_object($user)) + $list[] = sprintf("%s <%s>", $user->getName(), $user->getEmail()); + } + + return $list ? implode(', ', $list) : ''; + } + + function getEmails() { + $list = array(); + foreach($this->storage as $user) { + if (is_object($user)) + $list[] = $user->getEmail(); + } + return $list ? implode(', ', $list) : ''; + } + + static function getVarScope() { + return array( + 'names' => __('List of names'), + 'emails' => __('List of email addresses'), + 'full' => __('List of names and email addresses'), + ); + } +} diff --git a/include/staff/templates.inc.php b/include/staff/templates.inc.php index 0ed4dea249352587ccc084b28e835ee58e0ed102..e261a903c759d39c4ceff7fc8e62059292daf8ac 100644 --- a/include/staff/templates.inc.php +++ b/include/staff/templates.inc.php @@ -106,7 +106,7 @@ else if($ids && in_array($row['tpl_id'],$ids)) $sel=true; - $default=($defaultTplId==$row['tpl_id'])?'<small class="fadded">('.__('System Default').')</small>':''; + $default=($defaultTplId==$row['tpl_id'])?'<small class="faded">('.__('System Default').')</small>':''; ?> <tr id="<?php echo $row['tpl_id']; ?>"> <td align="center"> diff --git a/include/staff/templates/tickets-actions.tmpl.php b/include/staff/templates/tickets-actions.tmpl.php index 0535a5af2832a7da0354b6fa53dc88373ff6f6ca..2735fc70ea690421fd24aa066e927829cd8792fa 100644 --- a/include/staff/templates/tickets-actions.tmpl.php +++ b/include/staff/templates/tickets-actions.tmpl.php @@ -71,7 +71,6 @@ $(function() { +'?count='+count +'&tids='+tids.join(',') +'&_uid='+new Date().getTime(); - console.log(tids); $.dialog(url, [201], function (xhr) { $.pjax.reload('#pjax-container'); }); diff --git a/include/staff/ticket-open.inc.php b/include/staff/ticket-open.inc.php index 42d97994832a8aa01d4c415bf5b441de9c6b03e0..96ee98bbdeac440975ad94301eae6b2959c16569 100644 --- a/include/staff/ticket-open.inc.php +++ b/include/staff/ticket-open.inc.php @@ -170,10 +170,10 @@ if ($_POST) <?php echo __('Ticket Notice');?>: </td> <td> - <select id="emailreply" name="emailreply"> - <option value="reply-all"><?php echo __('Alert All'); ?></option> - <option value="reply-user"><?php echo __('Alert to User'); ?></option> - <option value="0">— <?php echo __('Do Not Send Alert'); ?> —</option> + <select id="reply-to" name="reply-to"> + <option value="all"><?php echo __('Alert All'); ?></option> + <option value="user"><?php echo __('Alert to User'); ?></option> + <option value="none">— <?php echo __('Do Not Send Alert'); ?> —</option> </select> </td> </tr> diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index 717f0f2d0f265a5f80d8fe9a25a77b6cdd615965..e16900e2516aa8e42b4530bd912e0c282b986ac2 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -373,7 +373,7 @@ if($ticket->isOverdue()) else $recipients = 0; - echo sprintf('<span><a class="collaborators preview" + echo sprintf('<span><a class="manage-collaborators preview" href="#thread/%d/collaborators"><span id="t%d-recipients"><i class="icon-group"></i> (%s)</span></a></span>', $ticket->getThreadId(), $ticket->getThreadId(), @@ -696,7 +696,10 @@ if ($errors['err'] && isset($_POST['a'])) { id="post-note-tab"><?php echo __('Post Internal Note');?></a></li> </ul> <?php - if ($role->hasPerm(Ticket::PERM_REPLY)) { ?> + if ($role->hasPerm(Ticket::PERM_REPLY)) { + $replyTo = $_POST['reply-to'] ?: 'all'; + $emailReply = ($replyTo != 'none'); + ?> <form id="reply" class="tab_content spellcheck exclusive save" data-lock-object-id="ticket/<?php echo $ticket->getId(); ?>" data-lock-id="<?php echo $mylock ? $mylock->getId() : ''; ?>" @@ -714,127 +717,188 @@ if ($errors['err'] && isset($_POST['a'])) { <?php }?> <tbody id="to_sec"> - <?php - # XXX: Add user-to-name and user-to-email HTML ID#s - if ($addresses = Email::getAddresses(array('smtp' => true))){ - ?> <tr> <td width="120"> <label><strong><?php echo __('From'); ?>:</strong></label> </td> <td> - <select id="from_name" name="from_name"> + <select id="from_email_id" name="from_email_id"> <?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 + // Department email (default). + echo sprintf('<option value="%s" selected="selected">%s</option>', + $dept->getEmail()->getId(), + Format::htmlchars($dept->getEmail()->getAddress())); + // Optional SMTP addreses user can send email via + if (($emails = Email::getAddresses(array('smtp' => + true), false)) && count($emails)) { + echo '<option value="" + disabled="disabled"> </option>'; + $emailId = $_POST['from_email_id'] ?: 0; + foreach ($emails as $e) { + if ($dept->getEmail()->getId() == $e->getId()) + continue; + echo sprintf('<option value="%s" %s>%s</option>', + $e->getId(), + $e->getId() == $emailId ? + 'selected="selected"' : '', + Format::htmlchars($e->getAddress())); } - } ?> + } + ?> </select> </td> </tr> - <?php } ?> - <tr> + </tbody> + <tbody id="recipients"> + <tr id="user-row"> + <td width="120"> + <label><strong><?php echo __('Recipients'); ?>:</strong></label> + </td> + <td><a href="#tickets/<?php echo $ticket->getId(); ?>/user" + onclick="javascript: + $.userLookup('ajax.php/tickets/<?php echo $ticket->getId(); ?>/user', + function (user) { + window.location = 'tickets.php?id='<?php $ticket->getId(); ?> + }); + return false; + "><span ><?php + echo Format::htmlchars($ticket->getOwner()->getEmail()->getAddress()); + ?></span></a> + </td> + </tr> + <tr><td> </td> + <td> + <div style="margin-bottom:2px;"> + <?php + echo sprintf('<span><a id="show_ccs" + class="icon-caret-right"></i> %s </a> + + <a class="manage-collaborators preview noclick %s" + href="#thread/%d/collaborators"> + (%s)</a></span>', + __('Collaborators'), + $ticket->getNumCollaborators() + ? '' : 'hidden', + $ticket->getThreadId(), + sprintf(__('%s of %s'), + sprintf('<span + class="collabselection__count">%d</span>', + $ticket->getNumActiveCollaborators()), + sprintf('<span + class="collabselection__total">%d</span>', + $ticket->getNumCollaborators()) + ) + ); + ?> + </div> + <div id="ccs" class="hidden"> + <div> + <span style="margin: 10px 5px 1px 0;" class="faded pull-left"><?php echo __('Select or Add New Collaborators'); ?> </span> + <?php + if ($role->hasPerm(Ticket::PERM_REPLY)) { ?> + <span class="action-button pull-left" style="margin: 2px 0 5px 20px;" + data-dropdown="#action-dropdown-collaborators" + data-placement="bottom" + data-toggle="tooltip" + title="<?php echo __('Manage Collaborators'); ?>" + > + <i class="icon-caret-down pull-right"></i> + <a class="ticket-action" id="collabs-button" + data-redirect="tickets.php?id=<?php echo + $ticket->getId(); ?>" + href="#thread/<?php echo + $ticket->getThreadId(); ?>/collaborators"> + <i class="icon-group"></i></a> + </span> + <?php + } ?> + <span class="error"> <?php echo $errors['ccs']; ?></span> + </div> + <?php + if ($role->hasPerm(Ticket::PERM_REPLY)) { ?> + <div id="action-dropdown-collaborators" class="action-dropdown anchor-right"> + <ul> + <li><a class="manage-collaborators" + href="#thread/<?php echo + $ticket->getThreadId(); ?>/add-collaborator/addcc"><i + class="icon-plus"></i> <?php echo __('Add New'); ?></a> + <li><a class="manage-collaborators" + href="#thread/<?php echo + $ticket->getThreadId(); ?>/collaborators"><i + class="icon-cog"></i> <?php echo __('Manage Collaborators'); ?></a> + </ul> + </div> + <?php + } ?> + <div class="clear"> + <select id="collabselection" name="ccs[]" multiple="multiple" + data-placeholder="<?php + echo __('Select Active Collaborators'); ?>"> + <?php + $collabs = $ticket->getCollaborators(); + foreach ($collabs as $c) { + echo sprintf('<option value="%s" %s class="%s">%s</option>', + $c->getUserId(), + $c->isActive() ? + 'selected="selected"' : '', + $c->isActive() ? + 'active' : 'disabled', + $c->getName()); + } + ?> + </select> + </div> + </div> + </td> + </tr> + <tr> <td width="120"> - <label><strong><?php echo __('Reply To'); ?>:</strong></label> + <label><?php echo __('Reply To'); ?>:</label> </td> <td> <?php - //see who sent the last message and decide which option to select. - $lastUser = $ticket->getLastUserRespondent(); + // Supported Reply Types + $replyTypes = array( + 'all' => __('All Active Recipients'), + 'user' => sprintf('%s (%s)', + __('Ticket Owner'), + Format::htmlchars($ticket->getOwner()->getEmail())), + 'none' => sprintf('— %s —', + __('Do Not Email Reply')) + ); - if ($ticket->getOwnerId() == $lastUser->getId()) - $ticketUser = true; - else { - $collabs = $ticket->getThread()->getCollaborators(); - foreach ($collabs as $collab) { - if ($collab->getUserId() == $lastUser->getId() && $collab->isCc()) - $ccUser = true; - } - } - if ($ticketUser || $ccUser) - $emailReply = true; + $replyTo = $_POST['reply-to'] ?: 'all'; + $emailReply = ($replyTo != 'none'); ?> - <select id="emailreply" name="emailreply"> - <option value="reply-all"><?php echo __('Reply All'); ?></option> - <option value="reply-user"><?php echo __('Reply to User'); ?></option> - <option value="0" <?php echo !$emailReply ? 'selected="selected"' : ''; ?> - >— <?php echo __('Do Not Email Reply'); ?> —</option> + <select id="reply-to" name="reply-to"> + <?php + foreach ($replyTypes as $k => $v) { + echo sprintf('<option value="%s" %s>%s</option>', + $k, + ($k == $replyTo) ? + 'selected="selected"' : '', + $v); + } + ?> </select> <i class="help-tip icon-question-sign" href="#reply_types"></i> </td> - </tr> - </tbody> - <?php - if(1) { //Make CC optional feature? NO, for now. - ?> - <tbody id="cc_sec" - style="display:<?php echo $emailReply? 'table-row-group':'none'; ?>;"> - <tr id="user-row"> - <td width="120" style="padding-left:20px;"> - <label><?php echo __('User'); ?></label> - </td> - <td> - <label><?php echo User::lookup($ticket->user_id); ?></label> - </td> - </tr> - <?php $collaborators = $ticket->getThread()->getCollaborators(); - $cc_cids = array(); - foreach ($collaborators as $c) { - if ($c->flags & Collaborator::FLAG_CC && $c->flags & Collaborator::FLAG_ACTIVE) - $cc_cids[] = $c->user_id; - } - ?> - <tr id="cc-row"> - <td width="160" style="padding-left:20px;"><?php echo __('Cc'); ?></td> - <td> - <span> - <select class="collabSelections" name="ccs[]" id="cc_users" multiple="multiple" - data-placeholder="<?php echo __('Select Contacts'); ?>"> - <?php - foreach ($cc_cids as $u) { - if($u != $ticket->user_id) { - ?> - <option value="<?php echo $u; ?>" <?php - if (in_array($u, $cc_cids)) - echo 'selected="selected"'; ?>><?php echo User::lookup($u); ?> - </option> - <?php } } ?> - ?> - </select> - </span> - - <a class="inline button" style="overflow:inherit" href="#" - onclick="javascript: - $.userLookup('ajax.php/users/lookup/form', function (user) { - var newUser = new Option(user.name, user.id, true, true); - return $("#cc_users").append(newUser).trigger('change'); - }); - "><i class="icon-plus"></i> <?php echo __('Add New'); ?></a> - - <br/><span class="error"><?php echo $errors['ccs']; ?></span> - </td> </tr> </tbody> - <?php - } ?> <tbody id="resp_sec"> - <?php - if($errors['response']) {?> - <tr><td width="120"> </td><td class="error"><?php echo $errors['response']; ?> </td></tr> - <?php - }?> + <tr><td colspan="2"> </td></tr> <tr> <td width="120" style="vertical-align:top"> <label><strong><?php echo __('Response');?>:</strong></label> </td> <td> -<?php if ($cfg->isCannedResponseEnabled()) { ?> + <?php + if ($errors['response']) + echo sprintf('<div class="error">%s</div>', + $errors['response']); + + if ($cfg->isCannedResponseEnabled()) { ?> + <div> <select id="cannedResp" name="cannedResp"> <option value="0" selected="selected"><?php echo __('Select a canned response');?></option> <option value='original'><?php echo __('Original Message'); ?></option> @@ -848,8 +912,8 @@ if ($errors['err'] && isset($_POST['a'])) { } ?> </select> - <br> -<?php } # endif (canned-resonse-enabled) + </div> + <?php } # endif (canned-resonse-enabled) $signature = ''; switch ($thisstaff->getDefaultSignatureType()) { case 'dept': @@ -860,6 +924,7 @@ if ($errors['err'] && isset($_POST['a'])) { $signature = $thisstaff->getSignature(); break; } ?> + <div> <input type="hidden" name="draft_id" value=""/> <textarea name="response" id="response" cols="50" data-signature-field="signature" data-dept-id="<?php echo $dept->getId(); ?>" @@ -874,6 +939,7 @@ if ($errors['err'] && isset($_POST['a'])) { list($draft, $attrs) = Draft::getDraftAndDataAttrs('ticket.response', $ticket->getId(), $info['response']); echo $attrs; ?>><?php echo $_POST ? $info['response'] : $draft; ?></textarea> + </div> <div id="reply_form_attachments" class="attachments"> <?php print $response_form->getField('attachments')->render(); @@ -1158,9 +1224,20 @@ $(function() { }); }); - $("#emailreply").ready(function(){ - checkReply(); - }); + $(document).on('click', 'a.manage-collaborators', function(e) { + e.preventDefault(); + var url = 'ajax.php/'+$(this).attr('href').substr(1); + $.dialog(url, 201, function (xhr) { + var resp = $.parseJSON(xhr.responseText); + if (resp.user && !resp.users) + resp.users.push(resp.user); + // TODO: Process resp.users + $('.tip_box').remove(); + }, { + onshow: function() { $('#user-search').focus(); } + }); + return false; + }); // Post Reply or Note action buttons. $('a.post-response').click(function (e) { @@ -1186,70 +1263,54 @@ $(function() { return false; }); - $('#emailreply').on('change', function(){ - checkReply(); + $('#show_ccs').click(function() { + var show = $(this); + var collabs = $('a#managecollabs'); + $('#ccs').slideToggle('fast', function(){ + if ($(this).is(":hidden")) { + collabs.hide(); + show.removeClass('icon-caret-down').addClass('icon-caret-right'); + } else { + collabs.show(); + show.removeClass('icon-caret-right').addClass('icon-caret-down'); + } }); + return false; + }); - function checkReply() { - var value = $("#emailreply").val(); - switch (value) { - case "reply-all": - $('#user-row').show(); - $('#cc-row').show(); - break; - case "reply-user": - $('#user-row').show(); - $('#cc-row').hide(); - break; - default: - $('#user-row').show(); - $('#cc-row').show(); - break; - } - } -}); - -$(function() { - $('.collabSelections').on("select2:unselecting", function(e) { - var el = $(this); - var target = '#' + e.currentTarget.id; - var confirmation = confirm(__("Are you sure you want to remove the collaborator from receiving this reply?")); - if (confirmation == false) { - $(target).on("select2:opening", function(e) { - return false; - }); - return false; - } - -}); - - $('.collabSelections').select2({ - width: '350px', - minimumInputLength: 3, - ajax: { - url: "ajax.php/users/local", - dataType: 'json', - data: function (params) { - if (!params) { - params.term = 'test'; - } - return { - q: params.term, - }; - }, - processResults: function (data) { - return { - results: $.map(data, function (item) { - return { - text: item.name, - slug: item.slug, - id: item.id - } - }) - }; - } - } - }); + $('.collaborators.noclick').click(function() { + $('#show_ccs').trigger('click'); + }); + $('#collabselection').select2({ + width: '350px', + allowClear: true, + sorter: (data) => { + return data.filter(function (item) { + return !item.selected; + }); + }, + templateResult: (e) => { + var $e = $( + '<span><i class="icon-user"></i> ' + e.text + '</span>' + ); + return $e; + } + }).on("select2:unselecting", function(e) { + if (!confirm(__("Are you sure you want to DISABLE the collaborator?"))) + e.preventDefault(); + }).on("select2:selecting", function(e) { + if (!confirm(__("Are you sure you want to ENABLE the collaborator?"))) + e.preventDefault(); + }).on('change', function(e) { + var id = e.currentTarget.id; + var count = $('li.select2-selection__choice').length; + var total = $('#' + id +' option').length; + $('.' + id + '__count').html(count); + $('.' + id + '__total').html(total); + $('.' + id + '__total').parent().toggle((total)); + }).on('select2:opening select2:closing', function(e) { + $(this).parent().find('.select2-search__field').prop('disabled', true); + }); }); </script> diff --git a/scp/js/scp.js b/scp/js/scp.js index 508123366f03b860cf906dd727bb26b9ff95fc4d..e016087d3bfb0fb13260e7500736f6c40eb0535c 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -801,7 +801,7 @@ $.confirm = function(message, title, options) { .append($('<span class="buttons pull-left"></span>') .append($('<input type="button" class="close"/>') .attr('value', __('Cancel')) - .click(function() { hide(); }) + .click(function() { hide(); D.resolve(false); }) )).append($('<span class="buttons pull-right"></span>') .append($('<input type="button"/>') .attr('value', __('OK')) @@ -1031,12 +1031,11 @@ $(document).on('submit', 'form', function() { }); //Collaborators -$(document).on('click', 'a.collaborator, a.collaborators', function(e) { +$(document).on('click', 'a.collaborator, a.collaborators:not(.noclick)', function(e) { e.preventDefault(); var url = 'ajax.php/'+$(this).attr('href').substr(1); $.dialog(url, 201, function (xhr) { var resp = $.parseJSON(xhr.responseText); - $('input#t'+resp.id+'-emailcollab').show(); $('#t'+resp.id+'-recipients').text(resp.text); $('.tip_box').remove(); }, { diff --git a/scp/tickets.php b/scp/tickets.php index a3b7103f3784a30b54b11b5666fcbda796fb4529..444275482087101137b03c913c6a72957efe1d64 100644 --- a/scp/tickets.php +++ b/scp/tickets.php @@ -175,8 +175,9 @@ if($_POST && !$errors): if(!$errors['err'] && Banlist::isBanned($ticket->getEmail())) $errors['err']=__('Email is in banlist. Must be removed to reply.'); } - - if(!$errors && ($response=$ticket->postReply($vars, $errors, $_POST['emailreply']))) { + $alert = strcasecmp('none', $_POST['reply-to']); + if(!$errors && ($response=$ticket->postReply($vars, $errors, + $alert))) { $msg = sprintf(__('%s: Reply posted successfully'), sprintf(__('Ticket #%s'), sprintf('<a href="tickets.php?id=%d"><b>%s</b></a>', diff --git a/setup/test/tests/stubs.php b/setup/test/tests/stubs.php index 6a7b0d285ea0e9898f600365ca692f29c768f1c0..4cb35183c3ea3593ff0efc0b20babd1ad7f5e244 100644 --- a/setup/test/tests/stubs.php +++ b/setup/test/tests/stubs.php @@ -223,6 +223,8 @@ class Mail_mime { function setTXTBody() {} function setHTMLBody() {} function addCc() {} + function addTo() {} + function addBcc() {} } class mPDF {