dev/core#1750 Create separate email per contact and maintaining the file ids for...
authorTunbola Ogunwande <tunbolawande@yahoo.com>
Mon, 31 Aug 2020 12:34:37 +0000 (13:34 +0100)
committerTunbola Ogunwande <tunbolawande@yahoo.com>
Wed, 17 Feb 2021 17:20:28 +0000 (18:20 +0100)
CRM/Activity/BAO/Activity.php
templates/CRM/Contact/Form/Task/Email.hlp
templates/CRM/Contact/Form/Task/Email.tpl
tests/phpunit/CRM/Activity/BAO/ActivityTest.php

index 1508f7c9531dc2b96c394e6c3e374d04bc80021b..7cdca184da2b5ca56887996f3be0aeda74ad0c1e 100644 (file)
@@ -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)
    *
index cd1f0c7e403e39edc50687a26f01e7351444d3ef..2498458c78fb25d92305a8d14a986520a0cc13b1 100644 (file)
 {/if}
 {/htxt}
 
+{htxt id="id-to_email-title"}
+  {ts}To Address{/ts}
+{/htxt}
+{htxt id="id-to_email"}
+<p>{ts}Contacts in the "To" field will each receive one copy of this email, with any tokens respectively filled for their contact record.{/ts}</p>
+<p>{ts}"To" recipients will not see which other "To" recipients received an email, but they will see the list of "Cc" recipients.{/ts}</p>
+<p>{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}</p>
+{/htxt}
+
 {htxt id="id-token-subject-title"}
   {ts}Subject Tokens{/ts}
 {/htxt}
index 0ab5a8930edbe7f312ce43e4b4089d1c807e8ce8..b4d143ca67173e4474b6f97ed9bd895cffede965 100644 (file)
@@ -23,7 +23,7 @@
     <tr class="crm-contactEmail-form-block-recipient">
        <td class="label">{if $single eq false}{ts}Recipient(s){/ts}{else}{$form.to.label}{/if}</td>
        <td>
-         {$form.to.html}
+         {$form.to.html} {help id="id-to_email" file="CRM/Contact/Form/Task/Email.hlp"}
        </td>
     </tr>
     <tr class="crm-contactEmail-form-block-cc_id" {if !$form.cc_id.value}style="display:none;"{/if}>
index b6a39ba499c79c4d7eae473fae0023395bd4d75c..63d299314d84e7005ebe1d008cd9868828444d5f 100644 (file)
@@ -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.
    *