de.propeace.mailinglistsync/Civi/Mailinglistsync/ADGroupMailingList.php

585 lines
19 KiB
PHP

<?php /** @noinspection SpellCheckingInspection */
declare(strict_types=1);
namespace Civi\Mailinglistsync;
use CRM_Mailinglistsync_ExtensionUtil as E;
use Civi\Mailinglistsync\Exceptions\MailinglistException;
class ADGroupMailingList extends GroupMailingList {
const RECIPIENTS_ATTRIBUTE_MAP = [
// Map recipient attribute names to method names (get/update)
'sid' => '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 {
$contactEmail = MailingListRecipient::getContactById($contactId)
->getEmailId(self::LOCATION_TYPE);
$this->addContactToGroup($contactId, $contactEmail);
$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;
}
}