From 726e904be58206fa1af35eff3f52cbf4345daf17 Mon Sep 17 00:00:00 2001 From: Tunbola Ogunwande Date: Mon, 31 Aug 2020 13:34:37 +0100 Subject: [PATCH] dev/core#1750 Create separate email per contact and maintaining the file ids for all email attachments --- CRM/Activity/BAO/Activity.php | 61 +++++++- templates/CRM/Contact/Form/Task/Email.hlp | 9 ++ templates/CRM/Contact/Form/Task/Email.tpl | 2 +- .../phpunit/CRM/Activity/BAO/ActivityTest.php | 131 ++++++++++++++++++ 4 files changed, 199 insertions(+), 4 deletions(-) diff --git a/CRM/Activity/BAO/Activity.php b/CRM/Activity/BAO/Activity.php index 1508f7c953..7cdca184da 100644 --- a/CRM/Activity/BAO/Activity.php +++ b/CRM/Activity/BAO/Activity.php @@ -1063,9 +1063,6 @@ class CRM_Activity_BAO_Activity extends CRM_Activity_DAO_Activity { $from = "$fromDisplayName <$fromEmail>"; } - //create the meta level record first ( email activity ) - $activityID = self::createEmailActivity($userID, $subject, $html, $text, $additionalDetails, $campaignId, $attachments, $caseId); - $returnProperties = []; if (isset($messageToken['contact'])) { foreach ($messageToken['contact'] as $key => $value) { @@ -1118,6 +1115,8 @@ class CRM_Activity_BAO_Activity extends CRM_Activity_DAO_Activity { } $sent = $notSent = []; + $attachmentFileIds = []; + $firstActivityCreated = FALSE; foreach ($contactDetails as $values) { $contactId = $values['contact_id']; $emailAddress = $values['email']; @@ -1171,6 +1170,20 @@ class CRM_Activity_BAO_Activity extends CRM_Activity_DAO_Activity { } $sent = FALSE; + // To minimize storage requirements, only one copy of any file attachments uploaded to CiviCRM is kept, + // even when multiple contacts will receive separate emails from CiviCRM. + if (!empty($attachmentFileIds)) { + $attachments = array_merge_recursive($attachments, $attachmentFileIds); + } + + // Create email activity. + $activityID = self::createEmailActivity($userID, $tokenSubject, $tokenHtml, $tokenText, $additionalDetails, $campaignId, $attachments, $caseId); + + if ($firstActivityCreated == FALSE && !empty($attachments)) { + $attachmentFileIds = self::getAttachmentFileIds($activityID, $attachments); + $firstActivityCreated = TRUE; + } + if (self::sendMessage( $from, $userID, @@ -1193,6 +1206,48 @@ class CRM_Activity_BAO_Activity extends CRM_Activity_DAO_Activity { return [$sent, $activityID]; } + /** + * Returns a array of attachment key with matching file ID. + * + * The function searches for all file Ids added for the activity and returns an array that + * uses the attachment key as the key and the file ID in the database for that matching attachment + * key by comparing the file URI for that attachment to the matching file URI fetched from the + * database. Having the file id matched per attachment key helps not to create a new file entry + * when a new activity with these attachments when the email activity is created. + * + * @param int $activityID + * Activity Id. + * @param array $attachments + * Attachments. + * + * @return array + * Array of attachment key versus file Id. + */ + private static function getAttachmentFileIds($activityID, $attachments) { + $queryParams = [1 => [$activityID, 'Positive'], 2 => [CRM_Activity_DAO_Activity::getTableName(), 'String']]; + $query = "SELECT file_id, uri FROM civicrm_entity_file INNER JOIN civicrm_file ON civicrm_entity_file.file_id = civicrm_file.id +WHERE entity_id =%1 AND entity_table = %2"; + $dao = CRM_Core_DAO::executeQuery($query, $queryParams); + + $fileDetails = []; + while ($dao->fetch()) { + $fileDetails[$dao->uri] = $dao->file_id; + } + + $activityAttachments = []; + foreach ($attachments as $attachmentKey => $attachment) { + foreach ($fileDetails as $keyUri => $fileId) { + $path = explode('/', $attachment['uri']); + $filename = $path[count($path) - 1]; + if ($filename == $keyUri) { + $activityAttachments[$attachmentKey]['id'] = $fileId; + } + } + } + + return $activityAttachments; + } + /** * Send SMS. Returns: bool $sent, int $activityId, int $success (number of sent SMS) * diff --git a/templates/CRM/Contact/Form/Task/Email.hlp b/templates/CRM/Contact/Form/Task/Email.hlp index cd1f0c7e40..2498458c78 100644 --- a/templates/CRM/Contact/Form/Task/Email.hlp +++ b/templates/CRM/Contact/Form/Task/Email.hlp @@ -26,6 +26,15 @@ {/if} {/htxt} +{htxt id="id-to_email-title"} + {ts}To Address{/ts} +{/htxt} +{htxt id="id-to_email"} +

{ts}Contacts in the "To" field will each receive one copy of this email, with any tokens respectively filled for their contact record.{/ts}

+

{ts}"To" recipients will not see which other "To" recipients received an email, but they will see the list of "Cc" recipients.{/ts}

+

{ts}Any contacts in the "Cc" or "Bcc" fields will receive a copy, one for each "To" email, but with the tokens filled for the "To" contact.{/ts}

+{/htxt} + {htxt id="id-token-subject-title"} {ts}Subject Tokens{/ts} {/htxt} diff --git a/templates/CRM/Contact/Form/Task/Email.tpl b/templates/CRM/Contact/Form/Task/Email.tpl index 0ab5a8930e..b4d143ca67 100644 --- a/templates/CRM/Contact/Form/Task/Email.tpl +++ b/templates/CRM/Contact/Form/Task/Email.tpl @@ -23,7 +23,7 @@ {if $single eq false}{ts}Recipient(s){/ts}{else}{$form.to.label}{/if} - {$form.to.html} + {$form.to.html} {help id="id-to_email" file="CRM/Contact/Form/Task/Email.hlp"} diff --git a/tests/phpunit/CRM/Activity/BAO/ActivityTest.php b/tests/phpunit/CRM/Activity/BAO/ActivityTest.php index b6a39ba499..63d299314d 100644 --- a/tests/phpunit/CRM/Activity/BAO/ActivityTest.php +++ b/tests/phpunit/CRM/Activity/BAO/ActivityTest.php @@ -1566,6 +1566,137 @@ $text $mut->stop(); } + /** + * Checks that tokens are uniquely replaced for contacts. + */ + public function testSendEmailWillReplaceTokensUniquelyForEachContact() { + $contactId1 = $this->individualCreate(['last_name' => 'Red']); + $contactId2 = $this->individualCreate(['last_name' => 'Pink']); + + // create a logged in USER since the code references it for sendEmail user. + $this->createLoggedInUser(); + $session = CRM_Core_Session::singleton(); + $loggedInUser = $session->get('userID'); + $contact = $this->callAPISuccess('Contact', 'get', ['sequential' => 1, 'id' => ['IN' => [$contactId1, $contactId2]]]); + + // Create a campaign. + $result = $this->callAPISuccess('Campaign', 'create', [ + 'version' => $this->_apiversion, + 'title' => __FUNCTION__ . ' campaign', + ]); + $campaign_id = $result['id']; + + // Add contact tokens in subject, html , text. + $subject = __FUNCTION__ . ' subject' . '{contact.display_name}'; + $html = __FUNCTION__ . ' html' . '{contact.display_name}'; + $text = __FUNCTION__ . ' text' . '{contact.display_name}'; + $userID = $loggedInUser; + + CRM_Activity_BAO_Activity::sendEmail( + $contact['values'], + $subject, + $text, + $html, + $contact['values'][0]['email'], + $userID, + $from = __FUNCTION__ . '@example.com', + $attachments = NULL, + $cc = NULL, + $bcc = NULL, + $contactIds = array_column($contact['values'], 'id'), + $additionalDetails = NULL, + NULL, + $campaign_id + ); + $result = $this->callAPISuccess('activity', 'get', ['campaign_id' => $campaign_id]); + // An activity created for each of the two contacts + $this->assertEquals(2, $result['count']); + $id = 0; + foreach ($result['values'] as $activity) { + $htmlValue = str_replace('{contact.display_name}', $contact['values'][$id]['display_name'], $html); + $textValue = str_replace('{contact.display_name}', $contact['values'][$id]['display_name'], $text); + $subjectValue = str_replace('{contact.display_name}', $contact['values'][$id]['display_name'], $subject); + $details = "-ALTERNATIVE ITEM 0- +$htmlValue +-ALTERNATIVE ITEM 1- +$textValue +-ALTERNATIVE END- +"; + $this->assertEquals($activity['details'], $details, 'Activity details does not match.'); + $this->assertEquals($activity['subject'], $subjectValue, 'Activity subject does not match.'); + $id++; + } + } + + /** + * Checks that attachments are not duplicated for activities. + */ + public function testSendEmailDoesNotDuplicateAttachmentFileIdsForActivitiesCreated() { + $contactId1 = $this->individualCreate(['last_name' => 'Red']); + $contactId2 = $this->individualCreate(['last_name' => 'Pink']); + + // create a logged in USER since the code references it for sendEmail user. + $this->createLoggedInUser(); + $session = CRM_Core_Session::singleton(); + $loggedInUser = $session->get('userID'); + $contact = $this->callAPISuccess('Contact', 'get', ['sequential' => 1, 'id' => ['IN' => [$contactId1, $contactId2]]]); + + // Create a campaign. + $result = $this->callAPISuccess('Campaign', 'create', [ + 'version' => $this->_apiversion, + 'title' => __FUNCTION__ . ' campaign', + ]); + $campaign_id = $result['id']; + + $subject = __FUNCTION__ . ' subject'; + $html = __FUNCTION__ . ' html'; + $text = __FUNCTION__ . ' text'; + $userID = $loggedInUser; + + $filepath = Civi::paths()->getPath('[civicrm.files]/custom'); + $fileName = "test_email_create.txt"; + $fileUri = "{$filepath}/{$fileName}"; + // Create a file. + CRM_Utils_File::createFakeFile($filepath, 'Bananas do not bend themselves without a little help.', $fileName); + $attachments = [ + 'attachFile_1' => + [ + 'uri' => $fileUri, + 'type' => 'text/plain', + 'location' => $fileUri, + ], + ]; + + CRM_Activity_BAO_Activity::sendEmail( + $contact['values'], + $subject, + $text, + $html, + $contact['values'][0]['email'], + $userID, + $from = __FUNCTION__ . '@example.com', + $attachments, + $cc = NULL, + $bcc = NULL, + $contactIds = array_column($contact['values'], 'id'), + $additionalDetails = NULL, + NULL, + $campaign_id + ); + $result = $this->callAPISuccess('activity', 'get', ['campaign_id' => $campaign_id]); + // An activity created for each of the two contacts, i.e two activities. + $this->assertEquals(2, $result['count']); + $activityIds = array_column($result['values'], 'id'); + $result = $this->callAPISuccess('Activity', 'get', [ + 'return' => ['file_id'], + 'id' => ['IN' => $activityIds], + 'sequential' => 1, + ]); + + // Verify that the that both activities are linked to the same File Id. + $this->assertEquals($result['values'][0]['file_id'], $result['values'][1]['file_id']); + } + /** * Adds a case with one activity. * -- 2.25.1