diff --git a/1 b/1 new file mode 100644 index 000000000000..60f36c6e0bab --- /dev/null +++ b/1 @@ -0,0 +1,32 @@ +Add / make fit for purpose email.getlist api call + +The function CRM_Contact_Page_AJAX::getContactEmail is one of our earlier ajax attempts & this approach has been largely +replaced with entity Reference fields. In order to switch over we need to bring Email.getlist api to parity which means +1) searching on sortname first, if less than 10 results on emails include emails +2) appropriate respect for includeWildCardInName (this should already be in the generic getlist) +3) filter out on_hold, is_deceased, do_not_email +4) acl support (should already be part of the api). + +The trickiest of these to support is the first - because we need to avoid using a non-performant OR +My current solution is the idea of a fallback field to search if the search results are less than the limit. +in most cases this won't require a second query but when it does it should be fairly quick. + +# Please enter the commit message for your changes. Lines starting +# with '#' will be ignored, and an empty message aborts the commit. +# +# Date: Mon Apr 6 15:48:00 2020 +1200 +# +# On branch emailget +# Changes to be committed: +# modified: api/v3/Email.php +# modified: api/v3/Generic/Getlist.php +# modified: tests/phpunit/CRM/Contact/Form/Task/EmailCommonTest.php +# modified: tests/phpunit/api/v3/ContactTest.php +# modified: tests/phpunit/api/v3/EmailTest.php +# +# Changes not staged for commit: +# modified: api/v3/Email.php +# modified: templates/CRM/Contact/Form/Task/Email.tpl +# modified: tests/phpunit/CRM/Contact/Form/Task/EmailCommonTest.php +# modified: tests/phpunit/api/v3/EmailTest.php +# diff --git a/CRM/Activity/Form/Task/Email.php b/CRM/Activity/Form/Task/Email.php index 67a247ccd6b3..eb9a60e06ba8 100644 --- a/CRM/Activity/Form/Task/Email.php +++ b/CRM/Activity/Form/Task/Email.php @@ -21,25 +21,4 @@ class CRM_Activity_Form_Task_Email extends CRM_Activity_Form_Task { use CRM_Contact_Form_Task_EmailTrait; - /** - * Build all the data structures needed to build the form. - * - * @throws \CiviCRM_API3_Exception - */ - public function preProcess() { - CRM_Contact_Form_Task_EmailCommon::preProcessFromAddress($this); - parent::preProcess(); - $this->setContactIDs(); - $this->assign('single', $this->_single); - } - - /** - * List available tokens for this form. - * - * @return array - */ - public function listTokens() { - return CRM_Core_SelectValues::contactTokens(); - } - } diff --git a/CRM/Contact/Form/Task/Email.php b/CRM/Contact/Form/Task/Email.php index afd39cd3a3c4..911a060adb0d 100644 --- a/CRM/Contact/Form/Task/Email.php +++ b/CRM/Contact/Form/Task/Email.php @@ -29,6 +29,8 @@ class CRM_Contact_Form_Task_Email extends CRM_Contact_Form_Task { * @throws \CRM_Core_Exception */ public function preProcess() { + // @todo - more of the handling in this function should be move to the trait. Notably the title part is + // not set on other forms that share the trait. // store case id if present $this->_caseId = CRM_Utils_Request::retrieve('caseid', 'String', $this, FALSE); $this->_context = CRM_Utils_Request::retrieve('context', 'Alphanumeric', $this); @@ -64,22 +66,27 @@ public function preProcess() { if ($this->_context === 'search') { $this->_single = TRUE; } - CRM_Contact_Form_Task_EmailCommon::preProcessFromAddress($this); - - if (!$cid && $this->_context !== 'standalone') { - parent::preProcess(); - } - - $this->assign('single', $this->_single); - if (CRM_Core_Permission::check('administer CiviCRM')) { - $this->assign('isAdmin', 1); + if ($cid || $this->_context === 'standalone') { + // When search context is false the parent pre-process is not set. That avoids it changing the + // redirect url & attempting to set the search params of the form. It may have only + // historical significance. + $this->setIsSearchContext(FALSE); } + $this->traitPreProcess(); } + /** + * Stub function as EmailTrait calls this. + * + * @todo move some code from preProcess into here. + */ + public function setContactIDs() {} + /** * List available tokens for this form. * * @return array + * @throws \CRM_Core_Exception */ public function listTokens() { $tokens = CRM_Core_SelectValues::contactTokens(); diff --git a/CRM/Contact/Form/Task/EmailCommon.php b/CRM/Contact/Form/Task/EmailCommon.php index e0334ead758d..1244b44f3a0d 100644 --- a/CRM/Contact/Form/Task/EmailCommon.php +++ b/CRM/Contact/Form/Task/EmailCommon.php @@ -87,6 +87,7 @@ public static function preProcessFromAddress(&$form, $bounce = TRUE) { * @throws \CRM_Core_Exception */ public static function buildQuickForm(&$form) { + CRM_Core_Error::deprecatedFunctionWarning('This code is no longer used in core and will be removed'); $toArray = $ccArray = $bccArray = []; $suppressedEmails = 0; //here we are getting logged in user id as array but we need target contact id. CRM-5988 @@ -355,6 +356,8 @@ public static function formRule($fields, $dontCare, $self) { * @throws \Civi\API\Exception\UnauthorizedException */ public static function postProcess(&$form) { + CRM_Core_Error::deprecatedFunctionWarning('This code is no longer used in core and will be removed'); + self::bounceIfSimpleMailLimitExceeded(count($form->_contactIds)); // check and ensure that @@ -375,6 +378,8 @@ public static function postProcess(&$form) { * @throws \Civi\API\Exception\UnauthorizedException */ public static function submit(&$form, $formValues) { + CRM_Core_Error::deprecatedFunctionWarning('This code is no longer used in core and will be removed'); + self::saveMessageTemplate($formValues); $from = $formValues['from_email_address'] ?? NULL; @@ -558,6 +563,8 @@ public static function submit(&$form, $formValues) { * @throws \Civi\API\Exception\UnauthorizedException */ protected static function saveMessageTemplate($formValues) { + CRM_Core_Error::deprecatedFunctionWarning('This code is no longer used in core and will be removed'); + if (!empty($formValues['saveTemplate']) || !empty($formValues['updateTemplate'])) { $messageTemplate = [ 'msg_text' => $formValues['text_message'], @@ -586,6 +593,8 @@ protected static function saveMessageTemplate($formValues) { * The number of emails the user is attempting to send */ public static function bounceIfSimpleMailLimitExceeded($count) { + CRM_Core_Error::deprecatedFunctionWarning('This code is no longer used in core and will be removed'); + $limit = Civi::settings()->get('simple_mail_limit'); if ($count > $limit) { CRM_Core_Error::statusBounce(ts('Please do not use this task to send a lot of emails (greater than %1). Many countries have legal requirements when sending bulk emails and the CiviMail framework has opt out functionality and domain tokens to help meet these.', @@ -602,6 +611,8 @@ public static function bounceIfSimpleMailLimitExceeded($count) { * @return array */ protected static function getEmails($element): array { + CRM_Core_Error::deprecatedFunctionWarning('This code is no longer used in core and will be removed'); + $allEmails = explode(',', $element->getValue()); $return = []; foreach ($allEmails as $value) { diff --git a/CRM/Contact/Form/Task/EmailTrait.php b/CRM/Contact/Form/Task/EmailTrait.php index 0c4776b1fec8..7583ef742864 100644 --- a/CRM/Contact/Form/Task/EmailTrait.php +++ b/CRM/Contact/Form/Task/EmailTrait.php @@ -37,6 +37,10 @@ trait CRM_Contact_Form_Task_EmailTrait { */ public $_templates; + /** + * Array of contacts to emai. + */ + public $emailContactIDs = []; /** * Store "to" contact details. * @var array @@ -68,6 +72,63 @@ trait CRM_Contact_Form_Task_EmailTrait { */ public $_bccContactIds = []; + /** + * Is the form being loaded from a search action. + * + * @var bool + */ + public $isSearchContext = TRUE; + + /** + * Getter for isSearchContext. + * + * @return bool + */ + public function isSearchContext(): bool { + return $this->isSearchContext; + } + + /** + * Setter for isSearchContext. + * + * @param bool $isSearchContext + */ + public function setIsSearchContext(bool $isSearchContext) { + $this->isSearchContext = $isSearchContext; + } + + /** + * Build all the data structures needed to build the form. + * + * @throws \CiviCRM_API3_Exception + * @throws \CRM_Core_Exception + */ + public function preProcess() { + $this->traitPreProcess(); + } + + /** + * Call trait preProcess function. + * + * This function exists as a transitional arrangement so classes overriding + * preProcess can still call it. Ideally it will be melded into preProcess later. + * + * @throws \CiviCRM_API3_Exception + * @throws \CRM_Core_Exception + */ + protected function traitPreProcess() { + CRM_Contact_Form_Task_EmailCommon::preProcessFromAddress($this); + if ($this->isSearchContext()) { + // Currently only the contact email form is callable outside search context. + parent::preProcess(); + } + $this->setContactIDs(); + $this->assign('single', $this->_single); + if (CRM_Core_Permission::check('administer CiviCRM')) { + $this->assign('isAdmin', 1); + } + } + /** * Build the form object. * @@ -78,7 +139,224 @@ public function buildQuickForm() { $this->assign('suppressForm', FALSE); $this->assign('emailTask', TRUE); - CRM_Contact_Form_Task_EmailCommon::buildQuickForm($this); + $toArray = $ccArray = $bccArray = []; + $suppressedEmails = 0; + //here we are getting logged in user id as array but we need target contact id. CRM-5988 + $cid = $this->get('cid'); + if ($cid) { + $this->_contactIds = explode(',', $cid); + } + if (count($this->_contactIds) > 1) { + $this->_single = FALSE; + } + $this->bounceIfSimpleMailLimitExceeded(count($this->_contactIds)); + + $emailAttributes = [ + 'class' => 'huge', + ]; + $to = $this->add('text', 'to', ts('To'), $emailAttributes, TRUE); + $cc = $this->add('text', 'cc_id', ts('CC'), $emailAttributes); + + $this->addEntityRef('bcc_id', ts('BCC'), [ + 'entity' => 'Email', + 'multiple' => TRUE, + 'api'=> ['params' => ['on_hold' => 0, 'contact_id.do_not_email' => 0]] + ]); + $setDefaults = TRUE; + if (property_exists($this, '_context') && $this->_context === 'standalone') { + $setDefaults = FALSE; + } + + $this->_allContactIds = $this->_toContactIds = $this->_contactIds; + if ($to->getValue()) { + $this->_toContactIds = $this->_contactIds = []; + foreach ($this->getEmails($to) as $value) { + if (!empty($value['contact_id'])) { + $this->_contactIds[] = $this->_toContactIds[] = $value['contact_id']; + $this->_toContactEmails[] = $value['email']; + } + + } + } + if ($cc->getValue()) { + foreach ($this->getEmails($cc) as $value) { + if (!empty($value['contact_id'])) { + $this->_ccContactIds[] = $value['contact_id']; + } + + } + } + + $this->_allContactIds = array_unique(array_merge($this->_contactIds, $this->_ccContactIds, $this->_bccContactIds)); + $setDefaults = empty($this->_allContactIds) ? $setDefaults: TRUE; + + //get the group of contacts as per selected by user in case of Find Activities + if (!empty($this->_activityHolderIds)) { + $contact = $this->get('contacts'); + $this->_allContactIds = $this->_contactIds = $contact; + } + + // check if we need to setdefaults and check for valid contact emails / communication preferences + if (is_array($this->_allContactIds) && $setDefaults) { + $returnProperties = [ + 'sort_name' => 1, + 'email' => 1, + 'do_not_email' => 1, + 'is_deceased' => 1, + 'on_hold' => 1, + 'display_name' => 1, + 'preferred_mail_format' => 1, + ]; + + // get the details for all selected contacts ( to, cc and bcc contacts ) + list($this->_contactDetails) = CRM_Utils_Token::getTokenDetails($this->_allContactIds, + $returnProperties, + FALSE, + FALSE + ); + + // make a copy of all contact details + $this->_allContactDetails = $this->_contactDetails; + + // perform all validations on unique contact Ids + foreach (array_unique($this->_allContactIds) as $key => $contactId) { + $value = $this->_contactDetails[$contactId]; + if ($value['do_not_email'] || empty($value['email']) || !empty($value['is_deceased']) || $value['on_hold']) { + $suppressedEmails++; + + // unset contact details for contacts that we won't be sending email. This is prevent extra computation + // during token evaluation etc. + unset($this->_contactDetails[$contactId]); + } + else { + $email = $value['email']; + + // build array's which are used to setdefaults + if (in_array($contactId, $this->_toContactIds)) { + $this->_toContactDetails[$contactId] = $this->_contactDetails[$contactId]; + // If a particular address has been specified as the default, use that instead of contact's primary email + if (!empty($this->_toEmail) && $this->_toEmail['contact_id'] == $contactId) { + $email = $this->_toEmail['email']; + } + $toArray[] = [ + 'text' => '"' . $value['sort_name'] . '" <' . $email . '>', + 'id' => "$contactId::{$email}", + ]; + } + elseif (in_array($contactId, $this->_ccContactIds)) { + $ccArray[] = [ + 'text' => '"' . $value['sort_name'] . '" <' . $email . '>', + 'id' => "$contactId::{$email}", + ]; + } + elseif (in_array($contactId, $this->_bccContactIds)) { + $bccArray[] = [ + 'text' => '"' . $value['sort_name'] . '" <' . $email . '>', + 'id' => "$contactId::{$email}", + ]; + } + } + } + + if (empty($toArray)) { + CRM_Core_Error::statusBounce(ts('Selected contact(s) do not have a valid email address, or communication preferences specify DO NOT EMAIL, or they are deceased or Primary email address is On Hold.')); + } + } + + $this->assign('toContact', json_encode($toArray)); + $this->assign('ccContact', json_encode($ccArray)); + $this->assign('bccContact', json_encode($bccArray)); + + $this->assign('suppressedEmails', $suppressedEmails); + + $this->assign('totalSelectedContacts', count($this->_contactIds)); + + $this->add('text', 'subject', ts('Subject'), 'size=50 maxlength=254', TRUE); + + $this->add('select', 'from_email_address', ts('From'), $this->_fromEmails, TRUE); + + CRM_Mailing_BAO_Mailing::commonCompose($this); + + // add attachments + CRM_Core_BAO_File::buildAttachment($this, NULL); + + if ($this->_single) { + // also fix the user context stack + if ($this->_caseId) { + $ccid = CRM_Core_DAO::getFieldValue('CRM_Case_DAO_CaseContact', $this->_caseId, + 'contact_id', 'case_id' + ); + $url = CRM_Utils_System::url('civicrm/contact/view/case', + "&reset=1&action=view&cid={$ccid}&id={$this->_caseId}" + ); + } + elseif ($this->_context) { + $url = CRM_Utils_System::url('civicrm/dashboard', 'reset=1'); + } + else { + $url = CRM_Utils_System::url('civicrm/contact/view', + "&show=1&action=browse&cid={$this->_contactIds[0]}&selectedChild=activity" + ); + } + + $session = CRM_Core_Session::singleton(); + $session->replaceUserContext($url); + $this->addDefaultButtons(ts('Send Email'), 'upload', 'cancel'); + } + else { + $this->addDefaultButtons(ts('Send Email'), 'upload'); + } + + $fields = [ + 'followup_assignee_contact_id' => [ + 'type' => 'entityRef', + 'label' => ts('Assigned to'), + 'attributes' => [ + 'multiple' => TRUE, + 'create' => TRUE, + 'api' => ['params' => ['is_deceased' => 0]], + ], + ], + 'followup_activity_type_id' => [ + 'type' => 'select', + 'label' => ts('Followup Activity'), + 'attributes' => ['' => '- ' . ts('select activity') . ' -'] + CRM_Core_PseudoConstant::ActivityType(FALSE), + 'extra' => ['class' => 'crm-select2'], + ], + 'followup_activity_subject' => [ + 'type' => 'text', + 'label' => ts('Subject'), + 'attributes' => CRM_Core_DAO::getAttribute('CRM_Activity_DAO_Activity', + 'subject' + ), + ], + ]; + + //add followup date + $this->add('datepicker', 'followup_date', ts('in')); + + foreach ($fields as $field => $values) { + if (!empty($fields[$field])) { + $attribute = $values['attributes'] ?? NULL; + $required = !empty($values['required']); + + if ($values['type'] === 'select' && empty($attribute)) { + $this->addSelect($field, ['entity' => 'activity'], $required); + } + elseif ($values['type'] === 'entityRef') { + $this->addEntityRef($field, $values['label'], $attribute, $required); + } + else { + $this->add($values['type'], $field, $values['label'], $attribute, $required, CRM_Utils_Array::value('extra', $values)); + } + } + } + + //Added for CRM-15984: Add campaign field + CRM_Campaign_BAO_Campaign::addCampaign($this); + + $this->addFormRule(['CRM_Contact_Form_Task_EmailCommon', 'formRule'], $this); + CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'templates/CRM/Contact/Form/Task/EmailCommon.js', 0, 'html-header'); } /** @@ -89,7 +367,267 @@ public function buildQuickForm() { * @throws \Civi\API\Exception\UnauthorizedException */ public function postProcess() { - CRM_Contact_Form_Task_EmailCommon::postProcess($this); + $this->bounceIfSimpleMailLimitExceeded(count($this->_contactIds)); + + // check and ensure that + $formValues = $this->controller->exportValues($this->getName()); + $this->submit($formValues); + } + + /** + * Bounce if there are more emails than permitted. + * + * @param int $count + * The number of emails the user is attempting to send + */ + protected function bounceIfSimpleMailLimitExceeded($count) { + $limit = Civi::settings()->get('simple_mail_limit'); + if ($count > $limit) { + CRM_Core_Error::statusBounce(ts('Please do not use this task to send a lot of emails (greater than %1). Many countries have legal requirements when sending bulk emails and the CiviMail framework has opt out functionality and domain tokens to help meet these.', + [1 => $limit] + )); + } + } + + /** + * Submit the form values. + * + * This is also accessible for testing. + * + * @param array $formValues + * + * @throws \CRM_Core_Exception + * @throws \CiviCRM_API3_Exception + * @throws \Civi\API\Exception\UnauthorizedException + */ + public function submit($formValues) { + $this->saveMessageTemplate($formValues); + + // dev/core#357 User Emails are keyed by their id so that the Signature is able to be added + // If we have had a contact email used here the value returned from the line above will be the + // numerical key where as $from for use in the sendEmail in Activity needs to be of format of "To Name" + $from = CRM_Utils_Mail::formatFromAddress($this); + $subject = $formValues['subject']; + + // CRM-13378: Append CC and BCC information at the end of Activity Details and format cc and bcc fields + $elements = ['cc_id', 'bcc_id']; + $additionalDetails = NULL; + $ccValues = $bccValues = []; + foreach ($elements as $element) { + if (!empty($formValues[$element])) { + $allEmails = explode(',', $formValues[$element]); + foreach ($allEmails as $value) { + list($contactId, $email) = explode('::', $value); + $contactURL = CRM_Utils_System::url('civicrm/contact/view', "reset=1&force=1&cid={$contactId}", TRUE); + switch ($element) { + case 'cc_id': + $ccValues['email'][] = '"' . $this->_contactDetails[$contactId]['sort_name'] . '" <' . $email . '>'; + $ccValues['details'][] = "" . $this->_contactDetails[$contactId]['display_name'] . ""; + break; + + case 'bcc_id': + $bccValues['email'][] = '"' . $this->_contactDetails[$contactId]['sort_name'] . '" <' . $email . '>'; + $bccValues['details'][] = "" . $this->_contactDetails[$contactId]['display_name'] . ""; + break; + } + } + } + } + + $cc = $bcc = ''; + if (!empty($ccValues)) { + $cc = implode(',', $ccValues['email']); + $additionalDetails .= "\ncc : " . implode(", ", $ccValues['details']); + } + if (!empty($bccValues)) { + $bcc = implode(',', $bccValues['email']); + $additionalDetails .= "\nbcc : " . implode(", ", $bccValues['details']); + } + + // CRM-5916: prepend case id hash to CiviCase-originating emails’ subjects + if (isset($this->_caseId) && is_numeric($this->_caseId)) { + $hash = substr(sha1(CIVICRM_SITE_KEY . $this->_caseId), 0, 7); + $subject = "[case #$hash] $subject"; + } + + $attachments = []; + CRM_Core_BAO_File::formatAttachment($formValues, + $attachments, + NULL, NULL + ); + + // format contact details array to handle multiple emails from same contact + $formattedContactDetails = []; + $tempEmails = []; + foreach ($this->_contactIds as $key => $contactId) { + // if we dont have details on this contactID, we should ignore + // potentially this is due to the contact not wanting to receive email + if (!isset($this->_contactDetails[$contactId])) { + continue; + } + $email = $this->_toContactEmails[$key]; + // prevent duplicate emails if same email address is selected CRM-4067 + // we should allow same emails for different contacts + $emailKey = "{$contactId}::{$email}"; + if (!in_array($emailKey, $tempEmails)) { + $tempEmails[] = $emailKey; + $details = $this->_contactDetails[$contactId]; + $details['email'] = $email; + unset($details['email_id']); + $formattedContactDetails[] = $details; + } + } + + $contributionIds = []; + if ($this->getVar('_contributionIds')) { + $contributionIds = $this->getVar('_contributionIds'); + } + + // send the mail + list($sent, $activityId) = CRM_Activity_BAO_Activity::sendEmail( + $formattedContactDetails, + $subject, + $formValues['text_message'], + $formValues['html_message'], + NULL, + NULL, + $from, + $attachments, + $cc, + $bcc, + array_keys($this->_toContactDetails), + $additionalDetails, + $contributionIds, + CRM_Utils_Array::value('campaign_id', $formValues), + $this->getVar('_caseId') + ); + + $followupStatus = ''; + if ($sent) { + $followupActivity = NULL; + if (!empty($formValues['followup_activity_type_id'])) { + $params['followup_activity_type_id'] = $formValues['followup_activity_type_id']; + $params['followup_activity_subject'] = $formValues['followup_activity_subject']; + $params['followup_date'] = $formValues['followup_date']; + $params['target_contact_id'] = $this->_contactIds; + $params['followup_assignee_contact_id'] = explode(',', $formValues['followup_assignee_contact_id']); + $followupActivity = CRM_Activity_BAO_Activity::createFollowupActivity($activityId, $params); + $followupStatus = ts('A followup activity has been scheduled.'); + + if (Civi::settings()->get('activity_assignee_notification')) { + if ($followupActivity) { + $mailToFollowupContacts = []; + $assignee = [$followupActivity->id]; + $assigneeContacts = CRM_Activity_BAO_ActivityAssignment::getAssigneeNames($assignee, TRUE, FALSE); + foreach ($assigneeContacts as $values) { + $mailToFollowupContacts[$values['email']] = $values; + } + + $sentFollowup = CRM_Activity_BAO_Activity::sendToAssignee($followupActivity, $mailToFollowupContacts); + if ($sentFollowup) { + $followupStatus .= '
' . ts("A copy of the follow-up activity has also been sent to follow-up assignee contacts(s)."); + } + } + } + } + + $count_success = count($this->_toContactDetails); + CRM_Core_Session::setStatus(ts('One message was sent successfully. ', [ + 'plural' => '%count messages were sent successfully. ', + 'count' => $count_success, + ]) . $followupStatus, ts('Message Sent', ['plural' => 'Messages Sent', 'count' => $count_success]), 'success'); + } + + // Display the name and number of contacts for those email is not sent. + // php 5.4 throws out a notice since the values of these below arrays are arrays. + // the behavior is not documented in the php manual, but it does the right thing + // suppressing the notices to get things in good shape going forward + $emailsNotSent = @array_diff_assoc($this->_allContactDetails, $this->_contactDetails); + + if ($emailsNotSent) { + $not_sent = []; + foreach ($emailsNotSent as $contactId => $values) { + $displayName = $values['display_name']; + $email = $values['email']; + $contactViewUrl = CRM_Utils_System::url('civicrm/contact/view', "reset=1&cid=$contactId"); + $not_sent[] = "$displayName" . ($values['on_hold'] ? '(' . ts('on hold') . ')' : ''); + } + $status = '(' . ts('because no email address on file or communication preferences specify DO NOT EMAIL or Contact is deceased or Primary email address is On Hold') . ')'; + CRM_Core_Session::setStatus($status, ts('One Message Not Sent', [ + 'count' => count($emailsNotSent), + 'plural' => '%count Messages Not Sent', + ]), 'info'); + } + + if (isset($this->_caseId)) { + // if case-id is found in the url, create case activity record + $cases = explode(',', $this->_caseId); + foreach ($cases as $key => $val) { + if (is_numeric($val)) { + $caseParams = [ + 'activity_id' => $activityId, + 'case_id' => $val, + ]; + CRM_Case_BAO_Case::processCaseActivity($caseParams); + } + } + } + } + + /** + * Save the template if update selected. + * + * @param array $formValues + * + * @throws \CiviCRM_API3_Exception + * @throws \Civi\API\Exception\UnauthorizedException + */ + protected function saveMessageTemplate($formValues) { + if (!empty($formValues['saveTemplate']) || !empty($formValues['updateTemplate'])) { + $messageTemplate = [ + 'msg_text' => $formValues['text_message'], + 'msg_html' => $formValues['html_message'], + 'msg_subject' => $formValues['subject'], + 'is_active' => TRUE, + ]; + + if (!empty($formValues['saveTemplate'])) { + $messageTemplate['msg_title'] = $formValues['saveTemplateName']; + CRM_Core_BAO_MessageTemplate::add($messageTemplate); + } + + if (!empty($formValues['template']) && !empty($formValues['updateTemplate'])) { + $messageTemplate['id'] = $formValues['template']; + unset($messageTemplate['msg_title']); + CRM_Core_BAO_MessageTemplate::add($messageTemplate); + } + } + } + + /** + * List available tokens for this form. + * + * @return array + */ + public function listTokens() { + return CRM_Core_SelectValues::contactTokens(); + } + + /** + * Get the emails from the added element. + * + * @param HTML_QuickForm_Element $element + * + * @return array + */ + protected function getEmails($element): array { + $allEmails = explode(',', $element->getValue()); + $return = []; + foreach ($allEmails as $value) { + $values = explode('::', $value); + $return[] = ['contact_id' => $values[0], 'email' => $values[1]]; + } + return $return; } } diff --git a/CRM/Contribute/Form/Task/Email.php b/CRM/Contribute/Form/Task/Email.php index f4819c6957b7..509c0e7dc056 100644 --- a/CRM/Contribute/Form/Task/Email.php +++ b/CRM/Contribute/Form/Task/Email.php @@ -21,19 +21,6 @@ class CRM_Contribute_Form_Task_Email extends CRM_Contribute_Form_Task { use CRM_Contact_Form_Task_EmailTrait; - /** - * Build all the data structures needed to build the form. - */ - public function preProcess() { - CRM_Contact_Form_Task_EmailCommon::preProcessFromAddress($this); - parent::preProcess(); - - // we have all the contribution ids, so now we get the contact ids - parent::setContactIDs(); - - $this->assign('single', $this->_single); - } - /** * List available tokens for this form. * diff --git a/CRM/Core/BAO/Email.php b/CRM/Core/BAO/Email.php index c8bcb650cf88..209b77989650 100644 --- a/CRM/Core/BAO/Email.php +++ b/CRM/Core/BAO/Email.php @@ -345,4 +345,24 @@ public static function del($id) { return CRM_Contact_BAO_Contact::deleteObjectWithPrimary('Email', $id); } + /** + * Get filters for entity reference fields. + * + * @return array + */ + public static function getEntityRefFilters() { + $contactFields = CRM_Contact_BAO_Contact::getEntityRefFilters(); + foreach ($contactFields as $index => &$contactField) { + if (!empty($contactField['entity'])) { + // For now email_getlist can't parse state, country etc. + unset($contactFields[$index]); + } + elseif ($contactField['key'] !== 'contact_id') { + $contactField['entity'] = 'Contact'; + $contactField['key'] = 'contact_id.' . $contactField['key']; + } + } + return $contactFields; + } + } diff --git a/CRM/Event/Form/Task/Email.php b/CRM/Event/Form/Task/Email.php index 2da1720e362d..cd0270b3764b 100644 --- a/CRM/Event/Form/Task/Email.php +++ b/CRM/Event/Form/Task/Email.php @@ -22,27 +22,4 @@ class CRM_Event_Form_Task_Email extends CRM_Event_Form_Task { use CRM_Contact_Form_Task_EmailTrait; - /** - * Build all the data structures needed to build the form. - */ - public function preProcess() { - CRM_Contact_Form_Task_EmailCommon::preProcessFromAddress($this); - parent::preProcess(); - - // we have all the participant ids, so now we get the contact ids - parent::setContactIDs(); - - $this->assign('single', $this->_single); - } - - /** - * List available tokens for this form. - * - * @return array - */ - public function listTokens() { - $tokens = CRM_Core_SelectValues::contactTokens(); - return $tokens; - } - } diff --git a/CRM/Member/Form/Task/Email.php b/CRM/Member/Form/Task/Email.php index aa6c7f177697..5e0e6d4baa61 100644 --- a/CRM/Member/Form/Task/Email.php +++ b/CRM/Member/Form/Task/Email.php @@ -22,32 +22,4 @@ class CRM_Member_Form_Task_Email extends CRM_Member_Form_Task { use CRM_Contact_Form_Task_EmailTrait; - /** - * Build all the data structures needed to build the form. - * - * @return void - * - * @throws \CRM_Core_Exception - * @throws \CiviCRM_API3_Exception - */ - public function preProcess() { - CRM_Contact_Form_Task_EmailCommon::preProcessFromAddress($this); - parent::preProcess(); - - // we have all the membership ids, so now we get the contact ids - parent::setContactIDs(); - - $this->assign('single', $this->_single); - } - - /** - * List available tokens for this form. - * - * @return array - */ - public function listTokens() { - $tokens = CRM_Core_SelectValues::contactTokens(); - return $tokens; - } - } diff --git a/api/v3/Email.php b/api/v3/Email.php index ad4f61995608..22230188c6f8 100644 --- a/api/v3/Email.php +++ b/api/v3/Email.php @@ -23,6 +23,9 @@ * * @return array * API result array + * + * @throws \API_Exception + * @throws \Civi\API\Exception\UnauthorizedException */ function civicrm_api3_email_create($params) { return _civicrm_api3_basic_create(_civicrm_api3_get_BAO(__FUNCTION__), $params, 'Email'); @@ -37,7 +40,6 @@ function civicrm_api3_email_create($params) { * Array of parameters determined by getfields. */ function _civicrm_api3_email_create_spec(&$params) { - // TODO a 'clever' default should be introduced $params['is_primary']['api.default'] = 0; $params['email']['api.required'] = 1; $params['contact_id']['api.required'] = 1; @@ -55,6 +57,10 @@ function _civicrm_api3_email_create_spec(&$params) { * * @return array * API result array. + * + * @throws \API_Exception + * @throws \CiviCRM_API3_Exception + * @throws \Civi\API\Exception\UnauthorizedException */ function civicrm_api3_email_delete($params) { return _civicrm_api3_basic_delete(_civicrm_api3_get_BAO(__FUNCTION__), $params); @@ -72,3 +78,32 @@ function civicrm_api3_email_delete($params) { function civicrm_api3_email_get($params) { return _civicrm_api3_basic_get(_civicrm_api3_get_BAO(__FUNCTION__), $params); } + +/** + * Set default getlist parameters. + * + * @see _civicrm_api3_generic_getlist_defaults + * + * @param array $request + * + * @return array + */ +function _civicrm_api3_email_getlist_defaults(&$request) { + return [ + 'description_field' => [ + 'contact_id.sort_name', + 'email', + ], + 'params' => [ + 'on_hold' => 0, + 'contact_id.is_deleted' => 0, + 'contact_id.is_deceased' => 0, + 'contact_id.do_not_email' => 0, + ], + 'label_field' => 'contact_id.display_name', + // If no results from sort_name try email. + 'search_field' => 'contact_id.sort_name', + 'search_field_fallback' => 'email', + ]; + +} diff --git a/api/v3/Generic/Getlist.php b/api/v3/Generic/Getlist.php index 464bd09be028..f1c344d54008 100644 --- a/api/v3/Generic/Getlist.php +++ b/api/v3/Generic/Getlist.php @@ -19,6 +19,7 @@ * @param array $apiRequest * * @return mixed + * @throws \CiviCRM_API3_Exception */ function civicrm_api3_generic_getList($apiRequest) { $entity = _civicrm_api_get_entity_name_from_camel($apiRequest['entity']); @@ -37,6 +38,23 @@ function civicrm_api3_generic_getList($apiRequest) { $request['params']['check_permissions'] = !empty($apiRequest['params']['check_permissions']); $result = civicrm_api3($entity, 'get', $request['params']); + if (!empty($request['input']) && !empty($defaults['search_field_fallback']) && $result['count'] < $request['params']['options']['limit']) { + // We support a field fallback. Note we don't do this as an OR query because that could easily + // bypass an index & kill the server. We just 'pad' the results if needed with the second + // query - this is effectively the same as what the old Ajax::getContactEmail function did. + // Since these queries should be quick & often only one should be needed this is a simpler alternative + // to constructing a UNION via the api. + $request['params'][$defaults['search_field_fallback']] = $request['params'][$defaults['search_field']]; + unset($request['params'][$defaults['search_field']]); + $request['params']['options']['limit'] -= $result['count']; + $result2 = civicrm_api3($entity, 'get', $request['params']); + $result['values'] = array_merge($result['values'], $result2['values']); + $result['count'] = count($result['values']); + } + else { + // Re-index to sequential = 0. + $result['values'] = array_merge($result['values']); + } // Hey api, would you like to format the output? $fnName = "_civicrm_api3_{$entity}_getlist_output"; @@ -98,7 +116,7 @@ function _civicrm_api3_generic_getList_defaults($entity, &$request, $apiDefaults $request += $apiDefaults + $defaults; // Default api params $params = [ - 'sequential' => 1, + 'sequential' => 0, 'options' => [], ]; // When searching e.g. autocomplete diff --git a/templates/CRM/Contact/Form/Task/Email.tpl b/templates/CRM/Contact/Form/Task/Email.tpl index 825dfaf8bc44..963fe6b3f651 100644 --- a/templates/CRM/Contact/Form/Task/Email.tpl +++ b/templates/CRM/Contact/Form/Task/Email.tpl @@ -130,7 +130,7 @@ CRM.$(function($) { {literal} emailSelect('#to', toContact); emailSelect('#cc_id', ccContact); - emailSelect('#bcc_id', bccContact); + //emailSelect('#bcc_id', bccContact); }); diff --git a/tests/phpunit/CRM/Contact/Form/Task/EmailCommonTest.php b/tests/phpunit/CRM/Contact/Form/Task/EmailCommonTest.php index 0326787d18ef..b0d68a8a2941 100644 --- a/tests/phpunit/CRM/Contact/Form/Task/EmailCommonTest.php +++ b/tests/phpunit/CRM/Contact/Form/Task/EmailCommonTest.php @@ -14,13 +14,18 @@ */ class CRM_Contact_Form_Task_EmailCommonTest extends CiviUnitTestCase { + /** + * Set up for tests. + * + * @throws \CRM_Core_Exception + */ protected function setUp() { parent::setUp(); $this->_contactIds = [ $this->individualCreate(['first_name' => 'Antonia', 'last_name' => 'D`souza']), $this->individualCreate(['first_name' => 'Anthony', 'last_name' => 'Collins']), ]; - $this->_optionValue = $this->callApiSuccess('optionValue', 'create', [ + $this->_optionValue = $this->callAPISuccess('optionValue', 'create', [ 'label' => '"Seamus Lee" ', 'option_group_id' => 'from_email_address', ]); @@ -28,6 +33,8 @@ protected function setUp() { /** * Test generating domain emails + * + * @throws \CRM_Core_Exception */ public function testDomainEmailGeneration() { $emails = CRM_Core_BAO_Email::domainEmails(); @@ -39,6 +46,13 @@ public function testDomainEmailGeneration() { $this->assertEquals('"Seamus Lee" ', $optionValue['values'][$this->_optionValue['id']]['label']); } + /** + * Test email uses signature. + * + * @throws \CRM_Core_Exception + * @throws \CiviCRM_API3_Exception + * @throws \Civi\API\Exception\UnauthorizedException + */ public function testPostProcessWithSignature() { $mut = new CiviMailUtils($this, TRUE); Civi::settings()->set('allow_mail_from_logged_in_contact', 1); @@ -66,9 +80,9 @@ public function testPostProcessWithSignature() { // This rule somehow disappears if there's a form-related test before us, // so register it again. See packages/HTML/QuickForm/file.php. $form->registerRule('maxfilesize', 'callback', '_ruleCheckMaxFileSize', 'HTML_QuickForm_file'); - CRM_Contact_Form_Task_EmailCommon::preProcessFromAddress($form); - CRM_Contact_Form_Task_EmailCommon::buildQuickForm($form); - CRM_Contact_Form_Task_EmailCommon::submit($form, array_merge($form->_defaultValues, [ + $form->preProcess(); + $form->buildQuickForm(); + $form->submit(array_merge($form->_defaultValues, [ 'from_email_address' => $loggedInEmail['id'], 'subject' => 'Really interesting stuff', ])); diff --git a/tests/phpunit/api/v3/ContactTest.php b/tests/phpunit/api/v3/ContactTest.php index 90f0e22e82f6..db3fd1efb170 100644 --- a/tests/phpunit/api/v3/ContactTest.php +++ b/tests/phpunit/api/v3/ContactTest.php @@ -3683,6 +3683,8 @@ public function testReturnCityProfile() { /** * CRM-15443 - ensure getlist api does not return deleted contacts. + * + * @throws \CRM_Core_Exception */ public function testGetlistExcludeConditions() { $name = 'Scarabée'; diff --git a/tests/phpunit/api/v3/EmailTest.php b/tests/phpunit/api/v3/EmailTest.php index 7f2e413936aa..a54183e71c6f 100644 --- a/tests/phpunit/api/v3/EmailTest.php +++ b/tests/phpunit/api/v3/EmailTest.php @@ -495,4 +495,26 @@ public function testSetBulkEmail() { $this->assertEquals(1, $emails[$email2['id']]['is_bulkmail']); } + /** + * Test getlist. + * + * @throws \CRM_Core_Exception + */ + public function testGetlist() { + $name = 'Scarabée'; + $emailMatchContactID = $this->individualCreate(['last_name' => $name, 'email' => 'bob@bob.com']); + $emailMatchEmailID = $this->callAPISuccessGetValue('Email', ['return' => 'id', 'contact_id' => $emailMatchContactID]); + $this->individualCreate(['last_name' => $name, 'email' => 'bob@bob.com', 'is_deceased' => 1]); + $this->individualCreate(['last_name' => $name, 'email' => 'bob@bob.com', 'is_deleted' => 1]); + $this->individualCreate(['last_name' => $name, 'api.email.create' => ['email' => 'bob@bob.com', 'on_hold' => 1]]); + $this->individualCreate(['last_name' => $name, 'do_not_email' => 1, 'api.email.create' => ['email' => 'bob@bob.com']]); + $nameMatchContactID = $this->individualCreate(['last_name' => 'bob', 'email' => 'blah@example.com']); + $nameMatchEmailID = $this->callAPISuccessGetValue('Email', ['return' => 'id', 'contact_id' => $nameMatchContactID]); + // We should get only the active live email-able contact. + $result = $this->callAPISuccess('Email', 'getlist', ['input' => 'bob'])['values']; + $this->assertCount(2, $result); + $this->assertEquals($nameMatchEmailID, $result[0]['id']); + $this->assertEquals($emailMatchEmailID, $result[1]['id']); + } + }