3 +--------------------------------------------------------------------+
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2018 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
29 * Class CRM_Mailing_BAO_MailingTest
31 class CRM_Mailing_BAO_MailingTest
extends CiviUnitTestCase
{
33 protected $allowedContactId = 0;
35 public function setUp() {
39 public function tearDown() {
42 CRM_Core_I18n_Schema
::makeSinglelingual('en_US');
48 * Helper function to assert whether the calculated recipients of a mailing
49 * match the expected list
52 * @param $expectedRecipients array
53 * Array of contact ID that should be in the recipient list.
55 private function assertRecipientsCorrect($mailingID, $expectedRecipients) {
57 // Reset keys to ensure match
58 $expectedRecipients = array_values($expectedRecipients);
60 // Load the recipients as a list of contact IDs
61 CRM_Mailing_BAO_Mailing
::getRecipients($mailingID);
62 $recipients = $this->callAPISuccess('MailingRecipients', 'get', array('mailing_id' => $mailingID));
63 $contactIDs = array();
64 foreach ($recipients['values'] as $recipient) {
65 $contactIDs[] = $recipient['contact_id'];
68 // Check the lists match
69 $this->assertTreeEquals($expectedRecipients, $contactIDs);
73 * Helper function to create a mailing include/exclude group.
80 private function createMailingGroup($mailingID, $groupID, $type = 'Include') {
81 return $this->callAPISuccess('MailingGroup', 'create', array(
82 'mailing_id' => $mailingID,
83 'group_type' => $type,
84 'entity_table' => CRM_Contact_BAO_Group
::getTableName(),
85 'entity_id' => $groupID,
90 * Test to ensure that using ACL permitted contacts are correctly fetched for bulk mailing
92 public function testgetRecipientsUsingACL() {
93 $this->prepareForACLs();
94 $this->createLoggedInUser();
95 // create hook to build ACL where clause which choses $this->allowedContactId as the only contact to be considered as mail recipient
96 $this->hookClass
->setHook('civicrm_aclWhereClause', array($this, 'aclWhereAllowedOnlyOne'));
97 CRM_Core_Config
::singleton()->userPermissionClass
->permissions
= array('access CiviCRM', 'view my contact');
99 // Create dummy group and assign 2 contacts
100 $name = 'Test static group ' . substr(sha1(rand()), 0, 7);
101 $groupID = $this->groupCreate([
106 // Create 2 contacts where one of them identified as $this->allowedContactId will be used in ACL where clause
107 $contactID1 = $this->individualCreate(array(), 0);
108 $this->allowedContactId
= $this->individualCreate(array(), 1);
109 $this->callAPISuccess('GroupContact', 'Create', array(
110 'group_id' => $groupID,
111 'contact_id' => $contactID1,
113 $this->callAPISuccess('GroupContact', 'Create', array(
114 'group_id' => $groupID,
115 'contact_id' => $this->allowedContactId
,
118 // Create dummy mailing
119 $mailingID = $this->callAPISuccess('Mailing', 'create', array())['id'];
120 $this->createMailingGroup($mailingID, $groupID);
122 // Check that the desired contact (identified as Contact ID - $this->allowedContactId) is the only
123 // contact chosen as mail recipient
124 $expectedContactIDs = [$this->allowedContactId
];
125 $this->assertRecipientsCorrect($mailingID, $expectedContactIDs);
127 $this->cleanUpAfterACLs();
128 $this->callAPISuccess('Group', 'Delete', ['id' => $groupID]);
129 $this->contactDelete($contactID1);
130 $this->contactDelete($this->allowedContactId
);
134 * Test mailing receipients when using previous mailing as include and contact is in exclude as well
136 public function testMailingIncludePreviousMailingExcludeGroup() {
137 $groupName = 'Test static group ' . substr(sha1(rand()), 0, 7);
138 $groupName2 = 'Test static group 2' . substr(sha1(rand()), 0, 7);
139 $groupID = $this->groupCreate([
140 'name' => $groupName,
141 'title' => $groupName,
144 $groupID2 = $this->groupCreate([
145 'name' => $groupName2,
146 'title' => $groupName2,
149 $contactID = $this->individualCreate(array(), 0);
150 $contactID2 = $this->individualCreate(array(), 2);
151 $this->callAPISuccess('GroupContact', 'Create', array(
152 'group_id' => $groupID,
153 'contact_id' => $contactID,
155 $this->callAPISuccess('GroupContact', 'Create', array(
156 'group_id' => $groupID,
157 'contact_id' => $contactID2,
159 $this->callAPISuccess('GroupContact', 'Create', array(
160 'group_id' => $groupID2,
161 'contact_id' => $contactID2,
163 // Create dummy mailing
164 $mailingID = $this->callAPISuccess('Mailing', 'create', array())['id'];
165 $this->createMailingGroup($mailingID, $groupID);
166 $expectedContactIDs = [$contactID, $contactID2];
167 $this->assertRecipientsCorrect($mailingID, $expectedContactIDs);
168 $mailingID2 = $this->callAPISuccess('Mailing', 'create', array())['id'];
169 $this->createMailingGroup($mailingID2, $groupID2, 'Exclude');
170 $this->callAPISuccess('MailingGroup', 'create', array(
171 'mailing_id' => $mailingID2,
172 'group_type' => 'Include',
173 'entity_table' => CRM_Mailing_BAO_Mailing
::getTableName(),
174 'entity_id' => $mailingID,
176 $expectedContactIDs = [$contactID];
177 $this->assertRecipientsCorrect($mailingID2, $expectedContactIDs);
178 $this->callAPISuccess('mailing', 'delete', ['id' => $mailingID2]);
179 $this->callAPISuccess('mailing', 'delete', ['id' => $mailingID]);
180 $this->callAPISuccess('group', 'delete', ['id' => $groupID]);
181 $this->callAPISuccess('group', 'delete', ['id' => $groupID2]);
182 $this->callAPISuccess('contact', 'delete', ['id' => $contactID, 'skip_undelete' => TRUE]);
183 $this->callAPISuccess('contact', 'delete', ['id' => $contactID2, 'skip_undelete' => TRUE]);
187 * Test verify that a disabled mailing group doesn't prvent access to the mailing generated with the group.
189 public function testGetMailingDisabledGroup() {
190 $this->prepareForACLs();
191 $this->createLoggedInUser();
192 // create hook to build ACL where clause which choses $this->allowedContactId as the only contact to be considered as mail recipient
193 $this->hookClass
->setHook('civicrm_aclWhereClause', array($this, 'aclWhereAllowedOnlyOne'));
194 $this->hookClass
->setHook('civicrm_aclGroup', array($this, 'hook_civicrm_aclGroup'));
195 CRM_Core_Config
::singleton()->userPermissionClass
->permissions
= array('access CiviCRM', 'edit groups');
196 // Create dummy group and assign 2 contacts
197 $name = 'Test static group ' . substr(sha1(rand()), 0, 7);
198 $groupID = $this->groupCreate([
203 $contactID = $this->individualCreate(array(), 0);
204 $this->callAPISuccess('GroupContact', 'Create', array(
205 'group_id' => $groupID,
206 'contact_id' => $contactID,
209 // Create dummy mailing
210 $mailingID = $this->callAPISuccess('Mailing', 'create', array())['id'];
211 $this->createMailingGroup($mailingID, $groupID);
212 // Now disable the group.
213 $this->callAPISuccess('group', 'create', [
217 $groups = CRM_Mailing_BAO_Mailing
::mailingACLIDs();
218 $this->assertTrue(in_array($groupID, $groups));
219 $this->cleanUpAfterACLs();
220 $this->contactDelete($contactID);
224 * Build ACL where clause
226 * @implements CRM_Utils_Hook::aclWhereClause
228 * @param string $type
229 * @param array $tables
230 * @param array $whereTables
231 * @param int $contactID
232 * @param string $where
234 public function aclWhereAllowedOnlyOne($type, &$tables, &$whereTables, &$contactID, &$where) {
235 $where = " contact_a.id = " . $this->allowedContactId
;
239 * Implements ACLGroup hook.
241 * @implements CRM_Utils_Hook::aclGroup
243 * aclGroup function returns a list of permitted groups
244 * @param string $type
245 * @param int $contactID
246 * @param string $tableName
247 * @param array $allGroups
248 * @param array $currentGroups
250 public function hook_civicrm_aclGroup($type, $contactID, $tableName, &$allGroups, &$currentGroups) {
251 //don't use api - you will get a loop
252 $sql = " SELECT * FROM civicrm_group";
254 $dao = CRM_Core_DAO
::executeQuery($sql);
255 while ($dao->fetch()) {
256 $groups[] = $dao->id
;
258 if (!empty($allGroups)) {
259 //all groups is empty if we really mean all groups but if a filter like 'is_disabled' is already applied
260 // it is populated, ajax calls from Manage Groups will leave empty but calls from New Mailing pass in a filtered list
261 $currentGroups = array_intersect($groups, array_flip($allGroups));
264 $currentGroups = $groups;
270 * @todo Missing tests:
271 * - Ensure opt out emails are not mailed
272 * - Ensure 'stop' emails are not mailed
273 * - Ensure the deceased are not mailed
274 * - Tests for getLocationFilterAndOrderBy (selecting correct 'type')
279 * Test to ensure that static and smart mailing groups can be added to an
280 * email mailing as 'include' or 'exclude' groups - and the members are
281 * included or excluded appropriately.
283 * contact 0 : static 0 (inc) + smart 5 (exc)
284 * contact 1 : static 0 (inc)
285 * contact 2 : static 1 (inc)
286 * contact 3 : static 1 (inc)
287 * contact 4 : static 2 (exc) + smart 3 (inc)
288 * contact 5 : smart 3 (inc)
289 * contact 6 : smart 4 (inc)
290 * contact 7 : smart 4 (inc)
291 * contact 8 : smart 5 (base)
293 * here 'contact 1 : static 0 (inc)' identified as static group $groupIDs[0]
294 * that has 'contact 1' identified as $contactIDs[0] and Included in the mailing recipient list
296 public function testgetRecipientsEmailGroupIncludeExclude() {
297 // Set up groups; 3 standard, 4 smart
299 for ($i = 0; $i < 7; $i++
) {
301 'name' => 'Test static group ' . $i,
302 'title' => 'Test static group ' . $i,
306 $groupIDs[$i] = $this->groupCreate($params);
309 $groupIDs[$i] = $this->smartGroupCreate(array(
310 'formValues' => ['last_name' => (($i == 6) ?
'smart5' : 'smart' . $i)],
317 $this->individualCreate(array('last_name' => 'smart5'), 0),
318 $this->individualCreate(array(), 1),
319 $this->individualCreate(array(), 2),
320 $this->individualCreate(array(), 3),
321 $this->individualCreate(array('last_name' => 'smart3'), 4),
322 $this->individualCreate(array('last_name' => 'smart3'), 5),
323 $this->individualCreate(array('last_name' => 'smart4'), 6),
324 $this->individualCreate(array('last_name' => 'smart4'), 7),
325 $this->individualCreate(array('last_name' => 'smart5'), 8),
328 // Add contacts to static groups
329 $this->callAPISuccess('GroupContact', 'Create', array(
330 'group_id' => $groupIDs[0],
331 'contact_id' => $contactIDs[0],
333 $this->callAPISuccess('GroupContact', 'Create', array(
334 'group_id' => $groupIDs[0],
335 'contact_id' => $contactIDs[1],
337 $this->callAPISuccess('GroupContact', 'Create', array(
338 'group_id' => $groupIDs[1],
339 'contact_id' => $contactIDs[2],
341 $this->callAPISuccess('GroupContact', 'Create', array(
342 'group_id' => $groupIDs[1],
343 'contact_id' => $contactIDs[3],
345 $this->callAPISuccess('GroupContact', 'Create', array(
346 'group_id' => $groupIDs[2],
347 'contact_id' => $contactIDs[4],
350 // Force rebuild the smart groups
351 for ($i = 3; $i < 7; $i++
) {
352 $group = new CRM_Contact_DAO_Group();
353 $group->id
= $groupIDs[$i];
355 CRM_Contact_BAO_GroupContactCache
::load($group, TRUE);
358 // Check that we can include static groups in the mailing.
359 // Expected: Contacts [0-3] should be included.
360 $mailing = $this->callAPISuccess('Mailing', 'create', array());
361 $this->createMailingGroup($mailing['id'], $groupIDs[0]);
362 $this->createMailingGroup($mailing['id'], $groupIDs[1]);
363 $this->createMailingGroup($mailing['id'], $groupIDs[6], 'Base');
364 $expected = $contactIDs;
365 unset($expected[4], $expected[5], $expected[6], $expected[7], $expected[8]);
366 $this->assertRecipientsCorrect($mailing['id'], $expected);
368 // Check that we can include smart groups in the mailing too.
369 // Expected: All contacts should be included.
370 // Also (dev/mail/6): Enable multilingual mode to check that restructing group doesn't affect recipient rebuilding
371 $this->enableMultilingual();
372 $this->createMailingGroup($mailing['id'], $groupIDs[3]);
373 $this->createMailingGroup($mailing['id'], $groupIDs[4]);
374 $this->createMailingGroup($mailing['id'], $groupIDs[5]);
375 // Check that all the contacts whould be present is recipient list as static group [0], [1] and [2] and
376 // smart groups [3], [4] and [5] is included in the recipient listing.
377 // NOTE: that contact[8] is present in both included smart group[5] and base smart group [6] so it will be
378 // present in recipient list as contact(s) from Base smart groups are not excluded the list as per (dev/mail/13)
379 $this->assertRecipientsCorrect($mailing['id'], $contactIDs);
381 // Check we can exclude static groups from the mailing.
382 // Expected: All contacts except [4]
383 $this->createMailingGroup($mailing['id'], $groupIDs[2], 'Exclude');
384 $expected = $contactIDs;
386 // NOTE: as per (dev/mail/13) if a contact A is present in smartGroup [5] which is Included in the mailing AND
387 // also present in another smartGroup [6] which is considered as Base group, then contact A should not be excluded from
388 // the recipient list due to later
389 $this->assertRecipientsCorrect($mailing['id'], $expected);
391 // Check we can exclude smart groups from the mailing too.
392 // Expected: All contacts except [0], [4] and [8]
393 $this->createMailingGroup($mailing['id'], $groupIDs[5], 'Exclude');
394 $expected = $contactIDs;
395 // As contact [0] and [8] belongs to excluded smart group[5] and base smart group[6] respectively,
396 // both these contacts should not be present in the mailing list
397 unset($expected[0], $expected[4], $expected[8]);
398 $this->assertRecipientsCorrect($mailing['id'], $expected);
400 // Tear down: delete mailing, groups, contacts
401 $this->deleteMailing($mailing['id']);
402 foreach ($groupIDs as $groupID) {
403 $this->groupDelete($groupID);
405 foreach ($contactIDs as $contactID) {
406 $this->contactDelete($contactID);
411 * Test CRM_Mailing_BAO_Mailing::getRecipients() on sms mode
413 public function testgetRecipientsSMS() {
414 // Tests for SMS bulk mailing recipients
415 // +CRM-21320 Ensure primary mobile number is selected over non-primary
418 $smartGroupParams = array(
419 'formValues' => array('contact_type' => array('IN' => array('Individual'))),
421 $group = $this->smartGroupCreate($smartGroupParams);
422 $sms_provider = $this->callAPISuccess('SmsProvider', 'create', array(
426 'username' => "Test",
427 'password' => "Test",
432 // Create Contact 1 and add in group
433 $contactID1 = $this->individualCreate(array(), 0);
434 $this->callAPISuccess('GroupContact', 'Create', array(
435 'group_id' => $group,
436 'contact_id' => $contactID1,
439 // Create contact 2 and add in group
440 $contactID2 = $this->individualCreate(array(), 1);
441 $this->callAPISuccess('GroupContact', 'Create', array(
442 'group_id' => $group,
443 'contact_id' => $contactID2,
446 $contactIDPhoneRecords = array(
447 $contactID1 => array(
448 'primary_phone_id' => CRM_Utils_Array
::value('id', $this->callAPISuccess('Phone', 'create', array(
449 'contact_id' => $contactID1,
451 'location_type_id' => "Home",
452 'phone_type_id' => "Mobile",
455 'other_phone_id' => CRM_Utils_Array
::value('id', $this->callAPISuccess('Phone', 'create', array(
456 'contact_id' => $contactID1,
458 'location_type_id' => "Work",
459 'phone_type_id' => "Mobile",
463 // Create the non-primary with a lower ID than the primary, to test CRM-21320
464 $contactID2 => array(
465 'other_phone_id' => CRM_Utils_Array
::value('id', $this->callAPISuccess('Phone', 'create', array(
466 'contact_id' => $contactID2,
468 'location_type_id' => "Home",
469 'phone_type_id' => "Mobile",
472 'primary_phone_id' => CRM_Utils_Array
::value('id', $this->callAPISuccess('Phone', 'create', array(
473 'contact_id' => $contactID2,
475 'location_type_id' => "Work",
476 'phone_type_id' => "Mobile",
482 // Prepare expected results
483 $checkPhoneIDs = array(
484 $contactID1 => $contactIDPhoneRecords[$contactID1]['primary_phone_id'],
485 $contactID2 => $contactIDPhoneRecords[$contactID2]['primary_phone_id'],
489 $mailing = $this->callAPISuccess('Mailing', 'create', array('sms_provider_id' => $sms_provider['id']));
490 $mailingInclude = $this->createMailingGroup($mailing['id'], $group);
493 CRM_Mailing_BAO_Mailing
::getRecipients($mailing['id']);
494 $recipients = $this->callAPISuccess('MailingRecipients', 'get', array('mailing_id' => $mailing['id']));
496 // Check the count is correct
497 $this->assertEquals(2, $recipients['count'], 'Check recipient count');
499 // Check we got the 'primary' mobile for both contacts
500 foreach ($recipients['values'] as $value) {
501 $this->assertEquals($value['phone_id'], $checkPhoneIDs[$value['contact_id']], 'Check correct phone number for contact ' . $value['contact_id']);
505 $this->deleteMailing($mailing['id']);
506 $this->callAPISuccess('SmsProvider', 'Delete', array('id' => $sms_provider['id']));
507 $this->groupDelete($group);
508 $this->contactDelete($contactID1);
509 $this->contactDelete($contactID2);
513 * Test alterMailingRecipients Hook which is called twice when we create a Mailing,
514 * 1. In the first call we will modify the mailing filter to include only deceased recipients
515 * 2. In the second call we will check if only deceased recipient is populated in MailingRecipient table
517 public function testAlterMailingRecipientsHook() {
518 $groupID = $this->groupCreate();
519 $this->tagCreate(array('name' => 'Tagged'));
521 // Create deseased Contact 1 and add in group
522 $contactID1 = $this->individualCreate(array('email' => 'abc@test.com', 'is_deceased' => 1), 0);
523 // Create deseased Contact 2 and add in group
524 $contactID2 = $this->individualCreate(array('email' => 'def@test.com'), 1);
525 // Create deseased Contact 3 and add in group
526 $contactID3 = $this->individualCreate(array('email' => 'ghi@test.com', 'is_deceased' => 1), 2);
528 // Add both the created contacts in group
529 $this->callAPISuccess('GroupContact', 'Create', array(
530 'group_id' => $groupID,
531 'contact_id' => $contactID1,
533 $this->callAPISuccess('GroupContact', 'Create', array(
534 'group_id' => $groupID,
535 'contact_id' => $contactID2,
537 $this->callAPISuccess('GroupContact', 'Create', array(
538 'group_id' => $groupID,
539 'contact_id' => $contactID3,
541 $this->entityTagAdd(array('contact_id' => $contactID3, 'tag_id' => 'Tagged'));
543 // trigger the alterMailingRecipients hook
544 $this->hookClass
->setHook('civicrm_alterMailingRecipients', array($this, 'alterMailingRecipients'));
546 // create mailing that will trigger alterMailingRecipients hook
548 'name' => 'mailing name',
549 'subject' => 'Test Subject',
550 'body_html' => '<p>HTML Body</p>',
551 'text_html' => 'Text Body',
553 'groups' => array('include' => array($groupID)),
554 'scheduled_date' => 'now',
556 $this->callAPISuccess('Mailing', 'create', $params);
560 * @implements CRM_Utils_Hook::alterMailingRecipients
562 * @param object $mailingObject
563 * @param array $criteria
564 * @param string $context
566 public function alterMailingRecipients(&$mailingObject, &$criteria, $context) {
567 if ($context == 'pre') {
568 // modify the filter to include only deceased recipient(s) that is Tagged
569 $criteria['is_deceased'] = CRM_Utils_SQL_Select
::fragment()->where("civicrm_contact.is_deceased = 1");
570 $criteria['tagged_contact'] = CRM_Utils_SQL_Select
::fragment()
571 ->join('civicrm_entity_tag', "INNER JOIN civicrm_entity_tag et ON et.entity_id = civicrm_contact.id AND et.entity_table = 'civicrm_contact'")
572 ->join('civicrm_tag', "INNER JOIN civicrm_tag t ON t.id = et.tag_id")
573 ->where("t.name = 'Tagged'");
576 $mailingRecipients = $this->callAPISuccess('MailingRecipients', 'get', array(
577 'mailing_id' => $mailingObject->id
,
578 'api.Email.getvalue' => array(
579 'id' => '$value.email_id',
583 $this->assertEquals(1, $mailingRecipients['count'], 'Check recipient count');
584 $this->assertEquals('ghi@test.com', $mailingRecipients['values'][$mailingRecipients['id']]['api.Email.getvalue'], 'Check if recipient email belong to deceased contact');