'Sid', 'email' => 'Email', 'first_name' => 'FirstName', 'last_name' => 'LastName', ]; public const LOCATION_TYPE = 'AD_Mailing_List_Address'; protected string $sid; /** * Get an AD mailing list by its SID. * * @params string $sid * @param string $sid * * @return \Civi\Mailinglistsync\ADGroupMailingList|null * * @throws \CRM_Core_Exception * @throws \Civi\API\Exception\UnauthorizedException * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException */ public static function getBySID(string $sid): ?self { $result = static::RELATED_CLASS::get() ->addSelect('id') ->addWhere(static::CUSTOM_GROUP_NAME . '.Active_Directory_SID', '=', $sid) ->execute() ->first(); return $result ? new self($result['id']) : NULL; } /** * Synchronize recipients with a list of SIDs and e-mail addresses. * * @param array $recipients * * @return array A description of the changes made * * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException */ public function syncRecipients(array $recipients): array { $adGroupMembers = $this->getRecipients(); // Add new recipients $recipientsAdded = []; foreach ($recipients as $recipient) { if (empty($adGroupMembers[$recipient['sid']] ?? NULL)) { $contactId = NULL; $recipientResult = $recipient + [ 'is_error' => 0, ]; // Check if the contact already exists $found = $this::findExistingContact( firstName: $recipient['first_name'], lastName: $recipient['last_name'], email: $recipient['email'], sid: $recipient['sid'], ); // If the contac does not exist, add it if ($found === NULL) { try { $newContact = MailingListRecipient::createContact( email: $recipient['email'], sid: $recipient['sid'], locationType: static::LOCATION_TYPE, firstName: $recipient['first_name'] ?? '', lastName: $recipient['last_name'] ?? '', ); $contactId = $newContact->getContactId(); $recipientResult['contact_id'] = $contactId; $recipientResult['contact_created'] = TRUE; // Create contact activity try { MailingListRecipient::createSyncActivity( $contactId, 'Completed', E::ts('Contact created by AD group synchronization'), E::ts("This contact has been created by the AD group synchronization process for the group '%1'", [1 => $this->getTitle()]) ); } catch (MailinglistException $e) { $error = $e->getLogMessage(); \Civi::log(E::LONG_NAME)->error($error); $recipientResult['is_error'] = 1; $recipientResult['activity_error_message'] = $error; } } catch (MailinglistException $e) { $recipientResult['is_error'] = 1; $recipientResult['error_message'] = $e->getLogMessage(); } } // If the contact exists, use the existing contact else { $contactId = $found['contact']['id']; $recipientResult['contact_id'] = $contactId; $resipientResult['found_by'] = $found['found_by']; $recipientResult['contact_created'] = FALSE; // If the contact was not found by SID, add the SID to the contact if ($found['found_by'] !== 'sid') { try { $contact = new MailingListRecipient(contact_id: $contactId); $contact->updateSid($recipient['sid']); $contact->save(); $recipientResult['updated'] = [ 'sid' => [ 'is_error' => 0, 'old' => $found['contact']['sid'], 'new' => $recipient['sid'], ], ]; } catch (MailinglistException $e) { $recipientResult['is_error'] = 1; $recipientResult['error_message'] = $e->getLogMessage(); } // Create contact activity try { MailingListRecipient::createSyncActivity( $contactId, 'Completed', E::ts('Contact updated by AD contact synchronization'), E::ts("This contact has been updated by the AD contact synchronization process") ); } catch (MailinglistException $e) { $error = $e->getLogMessage(); \Civi::log(E::LONG_NAME)->error($error); $recipientResult['is_error'] = 1; $recipientResult['activity_error_message'] = $error; } // Skip adding the contact to the group if it already is a member if (in_array( $contactId, array_column($adGroupMembers, 'contactId') )) { $recipientsAdded['recipients'][] = $recipientResult; continue; } } } // Add the contact to the mailing list if ($contactId !== NULL) { $recipientResult += $this->addRecipient($contactId); // Create contact activity try { MailingListRecipient::createSyncActivity( $contactId, 'Completed', E::ts('Contact added to group'), E::ts("This contact has been added to the group '%1' by AD group synchronization.", [1 => $this->getTitle()]) ); } catch (MailinglistException $e) { $error = $e->getLogMessage(); \Civi::log(E::LONG_NAME)->error($error); $recipientResult['is_error'] = 1; $recipientResult['activity_error_message'] = $error; } } else { $recipientResult['is_error'] = 1; $recipientResult['error_message'] = 'Contact ID is NULL'; } $recipientsAdded['recipients'][] = $recipientResult; } } $recipientsAdded['count'] = count($recipientsAdded['recipients'] ?? []); $recipientsAdded['error_count'] = count( array_filter($recipientsAdded['recipients'] ?? [], fn($recipient) => (bool) $recipient['is_error']) ); // Remove recipients that are no longer in the list $recipientsRemoved = []; foreach ($adGroupMembers as $adGroupMember) { /* @var \Civi\Mailinglistsync\MailingListRecipient $adGroupMember */ if (!in_array($adGroupMember->getSid(), array_column($recipients, 'sid'))) { $recipientsRemoved['recipients'][] = $this->removeRecipient($adGroupMember); // Create contact activity try { MailingListRecipient::createSyncActivity( $adGroupMember->getContactId(), 'Completed', E::ts('Contact removed from group'), E::ts("This contact has been removed from the group '%1' by AD group synchronization.", [1 => $this->getTitle()]) ); } catch (MailinglistException $e) { $error = $e->getLogMessage(); \Civi::log(E::LONG_NAME)->error($error); $recipientResult['is_error'] = 1; $recipientResult['activity_error_message'] = $error; } } } $recipientsRemoved['count'] = count($recipientsRemoved['recipients'] ?? []); $recipientsRemoved['error_count'] = count( array_filter($recipientsRemoved['recipients'] ?? [], fn($recipient) => (bool) $recipient['is_error']) ); // Update recipients $recipientsUpdated = []; foreach ($recipients as $recipient) { $recipientUpdated = $recipient; $changed = FALSE; // Find the group member by SID $adGroupMemberArray = array_filter( $this->getRecipients(), fn($adGroupMember) => $adGroupMember->getSid() === $recipient['sid'] ); $count = count($adGroupMemberArray); if ($count === 0) { continue; } elseif ($count > 1) { $recipientUpdated += [ 'is_error' => 1, 'error_message' => "Multiple recipients found with SID '{$recipient['sid']}'", ]; } /* @var \Civi\Mailinglistsync\MailingListRecipient $adGroupMember */ $adGroupMember = array_pop($adGroupMemberArray); // Create new email with AD_Mailing_List_Address location type if necessary if (!in_array(static::LOCATION_TYPE, $adGroupMember->getEmailLocationTypes())) { try { $adGroupMember->createEmail($recipient['email'], static::LOCATION_TYPE); $recipientUpdated['new_email_address_created'] = [ 'is_error' => 0, 'email' => $recipient['email'], 'location_type' => static::LOCATION_TYPE, ]; $changed = TRUE; } catch (MailinglistException $e) { $recipientUpdated['new_email_address_created']['is_error'] = 1; $recipientUpdated['new_email_address_created']['error_message'] = $e->getLogMessage(); } $recipientUpdated['is_error'] = $recipientUpdated['is_error'] || (int) $recipientUpdated['new_email_address_created']['is_error']; } // Update attributes if they have changed $recipientAttributes = self::RECIPIENTS_ATTRIBUTE_MAP; unset($recipientAttributes['sid']); foreach (self::RECIPIENTS_ATTRIBUTE_MAP as $attrName => $methodName) { $changed = self::updateRecipient( $recipient, $attrName, fn() => $adGroupMember->{"get$methodName"}(), fn($value) => $adGroupMember->{"update$methodName"}($value), $recipientUpdated, ) || $changed; } if ($changed) { // Apply changes try { $adGroupMember->save(); } catch (MailinglistException $e) { $recipientUpdated['is_error'] = 1; $recipientUpdated['error_message'] = $e->getLogMessage(); } $recipientUpdated['is_error'] = (int) ($recipientUpdated['is_error'] || !empty(array_filter( $recipientUpdated['updated'] ?? [], fn($update) => (bool) $update['is_error'] ))); } if ($changed) { $recipientsUpdated['recipients'][] = $recipientUpdated; } } $recipientsUpdated['count'] = count($recipientsUpdated['recipients'] ?? []); $recipientsUpdated['error_count'] = count( array_filter($recipientsUpdated['recipients'] ?? [], fn($recipient) => (bool) $recipient['is_error']) ); // Count results $resultCount = $recipientsAdded['count'] + $recipientsRemoved['count'] + $recipientsUpdated['count']; $errorCount = $recipientsAdded['error_count'] + $recipientsRemoved['error_count'] + $recipientsUpdated['error_count']; return [ 'count' => $resultCount, 'error_count' => $errorCount, 'is_error' => (int) ($errorCount > 0), 'added' => $recipientsAdded, 'removed' => $recipientsRemoved, 'updated' => $recipientsUpdated, ]; } /** * Updates contacts with values coming from the AD. * * @param $recipients * * @return array * @throws \Civi\Mailinglistsync\Exceptions\ContactSyncException * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException */ public static function syncContacts($recipients ): array { $results = []; foreach ($recipients as $contact) { $result = [ 'sid' => $contact['sid'] ?? 'unknown', 'first_name' => $contact['first_name'] ?? 'unknown', 'last_name' => $contact['last_name'] ?? 'unknown', 'email' => $contact['email'] ?? 'unknown', ]; // Check for required fields foreach (self::RECIPIENTS_ATTRIBUTE_MAP as $attrName => $_) { $missing = []; if (empty($contact[$attrName])) { $missing[] = $attrName; } } if (!empty($missing)) { $result += [ 'is_error' => 1, 'error_message' => E::ts('Missing required attributes: %1', [1 => implode(', ', $missing)]), ]; continue; } // Try to find an existing contact try { $contactSearch = self::findExistingContact( firstName: $contact['first_name'], lastName: $contact['last_name'], email: $contact['email'], sid: $contact['sid'], searchBy: ['sid'], ); } catch (MailinglistException $e) { $result += [ 'is_error' => 1, 'error_message' => $e->getLogMessage(), ]; continue; } if ($contactSearch === NULL) { $result += [ 'is_error' => 1, 'error_message' => "No contact found for SID '{$contact['sid']}'", ]; } // Update Contact else { $recipient = new MailingListRecipient( sid: $contact['sid'], contact_id: $contactSearch['contact']['id'], first_name: $contactSearch['contact']['first_name'], last_name: $contactSearch['contact']['last_name'], email: $contactSearch['contact']['email.email'], ); // Update attributes if they have changed; $changed = FALSE; $recipientAttributes = self::RECIPIENTS_ATTRIBUTE_MAP; unset($recipientAttributes['sid']); foreach (self::RECIPIENTS_ATTRIBUTE_MAP as $attrName => $methodName) { $changed = self::updateRecipient( $contact, $attrName, fn() => $recipient->{"get$methodName"}(), fn($value) => $recipient->{"update$methodName"}($value), $result, ) || $changed; } if ($changed) { // Apply changes try { $recipient->save(); $result += [ 'is_error' => 0, ]; } catch (MailinglistException $e) { $result += [ 'is_error' => 1, 'error_message' => $e->getLogMessage(), ]; } } // Count errors in updated attributes $result['error_count'] = count( array_filter($result['updated'] ?? [], fn($update) => (bool) $update['is_error']) ); $result['is_error'] = (int) (($result['is_error'] ?? FALSE) || $result['error_count'] > 0); } if ((($changed ?? FALSE) === TRUE) || $result['is_error']) { $results['updated'][] = $result; } } $results['count'] = count($results['updated'] ?? []); $results['error_count'] = array_sum(array_column($results['updated'] ?? [], 'is_error')); return ($results['count'] || $results['error_count']) ? $results : []; } /** * Get a list of recipients indexed by SID. * * @override GroupMailingList::getRecipients() * @return array * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException */ public function getRecipients(): array { $recipients = parent::getRecipients(); $recipientsBySid = []; foreach ($recipients as $recipient) { $recipientsBySid[$recipient->getSid()] = $recipient; } return $recipientsBySid; } /** * Add a recipient to the group related to this mailing list. * * @param int $contactId * * @return array|array[] */ private function addRecipient(int $contactId ): array { $result = []; // Add the contact to the group try { $this->addContactToGroup($contactId); $result['added'] = TRUE; } catch (MailinglistException $e) { $error = $e->getLogMessage(); \Civi::log(E::LONG_NAME)->error($error); $result['is_error'] = 1; $result['group_error_message'] = $error; } return $result; } /** * Remove a recipient from the group related to this mailing list. * * @param \Civi\Mailinglistsync\MailingListRecipient $recipient * * @return array */ public function removeRecipient(MailingListRecipient $recipient ): array { $result = [ 'sid' => $recipient->getSid(), 'contact_id' => $recipient->getContactId(), 'first_name' => $recipient->getFirstName(), 'last_name' => $recipient->getLastName(), 'email' => $recipient->getEmail(), ]; try { $this->removeContactFromGroup($recipient->getContactId()); $result['is_error'] = 0; // Create contact activity try { MailingListRecipient::createSyncActivity( $recipient->getContactId(), 'Completed', E::ts('Contact removed from mailing list'), E::ts("This contact has been removed from the mailing list '%1'", [1 => $this->getTitle()]) ); } catch (MailinglistException $e) { $error = $e->getLogMessage(); \Civi::log(E::LONG_NAME)->error($error); $result['is_error'] = 1; $result['activity_error_message'] = $error; } } catch (MailinglistException $e) { $error = $e->getLogMessage(); \Civi::log(E::LONG_NAME)->error($error); $result['is_error'] = 1; $result['error_message'] = $error; } finally { return $result; } } /** * Helper function to update recipient data dynamically. * OMG, is this DRY! * * @param array $new * @param string $attributeName * @param callable $getter * @param callable $setter * @param array $changes * * @return bool TRUE if the recipient was updated, FALSE otherwise */ private static function updateRecipient( array $new, string $attributeName, callable $getter, callable $setter, array &$changes, ): bool { $updated = FALSE; if (strtoupper($new[$attributeName]) !== strtoupper($getter())) { $error = NULL; $oldValue = $getter(); try { $setter($new[$attributeName]); $updated = TRUE; } catch (MailinglistException $e) { \Civi::log(E::LONG_NAME)->error($e->getLogMessage()); $error = $e->getLogMessage(); } finally { $changes['updated'][$attributeName] = [ 'is_error' => (int) ($error !== NULL), 'old' => $oldValue, 'new' => $new[$attributeName], ]; if ($error !== NULL) { $changes['updated'][$attributeName]['error_message'] = $error; } } } return $updated; } }