diff --git a/CRM/Mailinglistsync/Form/MailingListSettings.php b/CRM/Mailinglistsync/Form/MailingListSettings.php index feb30c6..00aee55 100644 --- a/CRM/Mailinglistsync/Form/MailingListSettings.php +++ b/CRM/Mailinglistsync/Form/MailingListSettings.php @@ -25,6 +25,12 @@ class CRM_Mailinglistsync_Form_MailingListSettings extends CRM_Core_Form { label: E::ts('Enable logging of synchronization'), extra: ['class' => 'huge'], ); + $this->add( + 'text', + E::SHORT_NAME . '_domain', + label: E::ts('Domain for mailing list email addresses'), + extra: ['class' => 'huge'], + ); $this->add( 'text', E::SHORT_NAME . '_mlmmj_host', diff --git a/CRM/Mailinglistsync/Upgrader.php b/CRM/Mailinglistsync/Upgrader.php index 41e1a78..c7ebbc8 100644 --- a/CRM/Mailinglistsync/Upgrader.php +++ b/CRM/Mailinglistsync/Upgrader.php @@ -32,7 +32,7 @@ class CRM_Mailinglistsync_Upgrader extends \CRM_Extension_Upgrader_Base { // $this->ctx->log->info(E::ts('Created %1 LocationType', [1 => $location['name']])); } catch (\CRM_Core_Exception $e) { - if ($e->getMessage() == 'DB Error: already exists') { + if ($e->getMessage() === 'DB Error: already exists') { // $this->ctx->log->info(E::ts('LocationType %1 already exists', [1 => $location['name']])); continue; } @@ -40,6 +40,28 @@ class CRM_Mailinglistsync_Upgrader extends \CRM_Extension_Upgrader_Base { throw $e; } } + + // Create activity types + $activityTypes = require_once E::path() . '/resources/activity_types.php'; + foreach ($activityTypes as $activity) { + try { + \Civi\Api4\OptionValue::create(FALSE) + ->addValue('option_group_id.name', 'activity_type') + ->addValue('label', $activity['label']) + ->addValue('name', $activity['name']) + ->addValue('description', $activity['description']) + ->execute(); +// $this->ctx->log->info(E::ts('Created %1 ActivityType', [1 => $activity['name']])); + } + catch (\CRM_Core_Exception $e) { + if ($e->getMessage() === 'DB Error: already exists') { +// $this->ctx->log->info(E::ts('ActivityType %1 already exists', [1 => $activity['name']])); + continue; + } +// $this->ctx->log->err(E::ts('Failed to create %1 ActivityType', [1 => $activity['name']])); + throw $e; + } + } } /** @@ -84,6 +106,16 @@ class CRM_Mailinglistsync_Upgrader extends \CRM_Extension_Upgrader_Base { ->execute(); // $this->ctx->log->info(E::ts('Enabled %1 CustomGroup', [1 => $customGroup])); } + + // Create scheduled job + \Civi\Api4\Job::create(FALSE) + ->addValue('run_frequency', 'Hourly') + ->addValue('name', 'Mlmmjsync') + ->addValue('api_entity', 'Mailinglistsync') + ->addValue('api_action', 'Mlmmjsync') + ->addValue('description', E::ts('Runs the synchronization between CiviCRM and mlmmj.')) + ->addValue('is_active', TRUE) + ->execute(); } /** @@ -100,6 +132,11 @@ class CRM_Mailinglistsync_Upgrader extends \CRM_Extension_Upgrader_Base { ->execute(); // $this->ctx->log->info(E::ts('Disabled %1 CustomGroup', [1 => $customGroup])); } + + // Delete scheduled job + \Civi\Api4\Job::delete(FALSE) + ->addWhere('name', '=', 'Mlmmjsync') + ->execute(); } /** @@ -108,12 +145,10 @@ class CRM_Mailinglistsync_Upgrader extends \CRM_Extension_Upgrader_Base { * @return TRUE on success * @throws CRM_Core_Exception */ - // public function upgrade_4200(): bool { - // $this->ctx->log->info('Applying update 4200'); - // CRM_Core_DAO::executeQuery('UPDATE foo SET bar = "whiz"'); - // CRM_Core_DAO::executeQuery('DELETE FROM bang WHERE willy = wonka(2)'); - // return TRUE; - // } + public function upgrade_4200(): bool { + $this->install(); + return TRUE; + } /** * Example: Run an external SQL script. diff --git a/Civi/Mailinglistsync/ADGroupMailingList.php b/Civi/Mailinglistsync/ADGroupMailingList.php index 4912e0b..5cbd063 100644 --- a/Civi/Mailinglistsync/ADGroupMailingList.php +++ b/Civi/Mailinglistsync/ADGroupMailingList.php @@ -1,4 +1,5 @@ 'Sid', + 'email' => 'Email', + 'first_name' => 'FirstName', + 'last_name' => 'LastName', + ]; + public const LOCATION_TYPE = 'AD_Mailing_List_Address'; protected string $sid; @@ -24,15 +33,13 @@ class ADGroupMailingList extends GroupMailingList { * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException */ public static function getBySID(string $sid): ?self { - $id = static::RELATED_CLASS::get() + $result = static::RELATED_CLASS::get() ->addSelect('id') ->addWhere(static::CUSTOM_GROUP_NAME . '.Active_Directory_SID', '=', $sid) ->execute() - ->first()['id']; + ->first(); - $groups = \CRM_Contact_BAO_Group::getGroups(['id' => $id]); - $groupId = array_pop($groups); - return $groupId ? new self($groupId) : NULL; + return $result ? new self($result['id']) : NULL; } /** @@ -45,42 +52,192 @@ class ADGroupMailingList extends GroupMailingList { * @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)) { - $recipientsAdded[$recipient['sid']] = $this->addRecipient($recipient); + $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[$adGroupMember->getSid()] = $this->removeRecipient($adGroupMember); + $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 = []; - $recipientAttributesMap = [ - // Map recipient attribute names to method names (get/set) - 'first_name' => 'FirstName', - 'last_name' => 'LastName', - 'email' => 'Email', - ]; - foreach ($recipients as $recipient) { + $recipientUpdated = $recipient; $changed = FALSE; // Find the group member by SID $adGroupMemberArray = array_filter( - $adGroupMembers, + $this->getRecipients(), fn($adGroupMember) => $adGroupMember->getSid() === $recipient['sid'] ); $count = count($adGroupMemberArray); @@ -88,54 +245,208 @@ class ADGroupMailingList extends GroupMailingList { continue; } elseif ($count > 1) { - throw new MailinglistException( - "Multiple recipients with the same SID", - MailinglistException::ERROR_CODE_MULTIPLE_RECIPIENTS - ); + $recipientUpdated += [ + 'is_error' => 1, + 'error_message' => "Multiple recipients found with SID '{$recipient['sid']}'", + ]; } /* @var \Civi\Mailinglistsync\MailingListRecipient $adGroupMember */ $adGroupMember = array_pop($adGroupMemberArray); - // Update attributes if they have changed - foreach ($recipientAttributesMap as $attrName => $methodName) { - $changed = self::updateRecipient( - $adGroupMember, - $recipient, - $attrName, - fn() => $adGroupMember->{"get$methodName"}(), - fn($value) => $adGroupMember->{"set$methodName"}($value), - $recipientsUpdated, - ) || $changed; + // 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; } - // Apply changes if ($changed) { - $adGroupMember->save(); + // 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; } } - // Count errors - $errorCount = count(array_filter( - $recipientsAdded, - fn($recipient) => $recipient['is_error'] - )); - $errorCount += count(array_filter( - $recipientsRemoved, - fn($recipient) => $recipient['is_error'] - )); - $errorCount += count(array_filter( - $recipientsUpdated, - fn($recipient) => array_filter($recipient, fn($attr) => $attr['is_error']) - )); + $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, - 'recipients_added' => $recipientsAdded, - 'recipients_removed' => $recipientsRemoved, - 'recipients_updated' => $recipientsUpdated, + '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 %1', + [1 => $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'] ?? [], 'error_count')); + return ($results['count'] || $results['error_count']) ? $results : []; + } + /** * Get a list of recipients indexed by SID. * @@ -143,47 +454,86 @@ class ADGroupMailingList extends GroupMailingList { * @return array * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException */ - public function getRecipients(): array { + public + function getRecipients(): array { $recipients = parent::getRecipients(); - return array_column($recipients, null, fn($recipient) => $recipient->getSid()); + $recipientsBySid = []; + foreach ($recipients as $recipient) { + $recipientsBySid[$recipient->getSid()] = $recipient; + } + return $recipientsBySid; } - private function addRecipient(array $recipient): array { - $result = ['recipient' => $recipient]; - + /** + * 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 { - // Try to find an existing contact - $contactSearch = $this->findExistingContact( - firstName: $recipient['first_name'], - lastName: $recipient['last_name'], - email: $recipient['email'], - ); - if ($contactSearch !== NULL) { - $result['is_error'] = FALSE; - $result['recipient']['contact_id'] = $contactSearch['contact']['id']; - $result['recipient']['found_by'] = $contactSearch['found_by']; - $result['recipient']['contact_created'] = FALSE; - return $result; - } - - // Create a new contact if none was found - $newRecipient = MailingListRecipient::createContact( - firstName: $recipient['first_name'], - lastName: $recipient['last_name'], - email: $recipient['email'], - sid: $recipient['sid'], - ); - $result['is_error'] = FALSE; - $result['recipient']['contact_id'] = $newRecipient->getContactId(); - $result['recipient']['contact_created'] = TRUE; - return $result; + $this->addContactToGroup($contactId); + $result['added'] = TRUE; } catch (MailinglistException $e) { $error = $e->getLogMessage(); \Civi::log(E::LONG_NAME)->error($error); - $result['is_error'] = TRUE; + $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; - $result['recipient']['contact_created'] = FALSE; + } + finally { return $result; } } @@ -192,7 +542,6 @@ class ADGroupMailingList extends GroupMailingList { * Helper function to update recipient data dynamically. * OMG, is this DRY! * - * @param \Civi\Mailinglistsync\MailingListRecipient $old * @param array $new * @param string $attributeName * @param callable $getter @@ -201,8 +550,8 @@ class ADGroupMailingList extends GroupMailingList { * * @return bool TRUE if the recipient was updated, FALSE otherwise */ - private static function updateRecipient( - MailingListRecipient $old, + private + static function updateRecipient( array $new, string $attributeName, callable $getter, @@ -210,7 +559,7 @@ class ADGroupMailingList extends GroupMailingList { array &$changes, ): bool { $updated = FALSE; - if ($new[$attributeName] !== $getter()) { + if (strtoupper($new[$attributeName]) !== strtoupper($getter())) { $error = NULL; $oldValue = $getter(); try { @@ -222,16 +571,17 @@ class ADGroupMailingList extends GroupMailingList { $error = $e->getLogMessage(); } finally { - $changes[$old['sid']][$attributeName] = [ - 'is_error' => $error !== NULL, + $changes['updated'][$attributeName] = [ + 'is_error' => (int) ($error !== NULL), 'old' => $oldValue, 'new' => $new[$attributeName], ]; if ($error !== NULL) { - $changes[$old['sid']][$attributeName]['error'] = $error; + $changes['updated'][$attributeName]['error_message'] = $error; } } } return $updated; } + } diff --git a/Civi/Mailinglistsync/BaseMailingList.php b/Civi/Mailinglistsync/BaseMailingList.php index bbd3f91..c928635 100644 --- a/Civi/Mailinglistsync/BaseMailingList.php +++ b/Civi/Mailinglistsync/BaseMailingList.php @@ -1,17 +1,17 @@ updateData = []; if ($entity) { $this->load($entity); } - $this->mailingListManager = new MailingListManager(); + $this->mailingListManager = MailingListManager::getInstance(); } /** @@ -54,7 +56,8 @@ abstract class BaseMailingList { * @return void * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException */ - protected function load(array|int $entity): void { + protected function load(array|int $entity): void + { if (is_int($entity)) { $id = $entity; try { @@ -64,16 +67,16 @@ abstract class BaseMailingList { ->addWhere('id', '=', $id) ->execute() ->first(); - $this->setEntity($entity); - } - catch (UnauthorizedException) { + if (!empty($entity)) { + $this->setEntity($entity); + } + } catch (UnauthorizedException) { $type = static::RELATED_TYPE; throw new MailinglistException( "Could not get $type with id '$id' via API4 due to insufficient permissions", MailinglistException::ERROR_CODE_PERMISSION_DENIED ); - } - catch (\Exception) { + } catch (\Exception) { $type = static::RELATED_TYPE; throw new MailinglistException( "Could not get $type with id '$id' via API4", @@ -90,7 +93,8 @@ abstract class BaseMailingList { * @throws UnauthorizedException * @throws \CRM_Core_Exception */ - static protected function getCustomFields(): array { + static protected function getCustomFields(): array + { return CustomField::get() ->addSelect('*') ->addWhere('custom_group_id:name', '=', static::CUSTOM_GROUP_NAME) @@ -98,6 +102,34 @@ abstract class BaseMailingList { ->getArrayCopy(); } + /** + * Maps field names from forms (e.g. custom_12_287) to custom field names. + * + * @param $fields + * @return array + * @throws UnauthorizedException + * @throws \CRM_Core_Exception + */ + static public function translateCustomFields($fields): array + { + $customFields = self::getCustomFields(); + $mapping = []; + foreach ($customFields as $customField) { + $fieldId = $customField['id']; + $fieldName = "custom_$fieldId"; + $getCustomField = function () use ($fields, $fieldName) { + foreach ($fields as $key => $value) { + if (preg_match("/^$fieldName" . "_.*/", $key)) { + return ['field_name' => $key, 'value' => $value]; + } + } + return NULL; + }; + $mapping[$customField['name']] = $getCustomField(); + } + return $mapping; + } + /** * Validate e-mail address. * @@ -105,10 +137,25 @@ abstract class BaseMailingList { * * @return bool */ - static public function validateEmailAddress(string $emailAddress): bool { + static public function validateEmailAddress(string $emailAddress): bool + { return (bool) filter_var($emailAddress, FILTER_VALIDATE_EMAIL); } + /** + * Checks if the domain of the e-mail address is the same as the configured + * domain. + * + * @param string $emailAddress + * + * @return bool + */ + static public function validateEmailDomain(string $emailAddress): bool + { + $domain = explode('@', $emailAddress)[1]; + return MailingListSettings::get('domain') === $domain; + } + /** * Validate custom fields. * @@ -121,7 +168,8 @@ abstract class BaseMailingList { * @throws \Civi\API\Exception\UnauthorizedException * @throws \Exception */ - static public function validateCustomFields(array $fields, \CRM_Core_Form $form, array &$errors): bool { + static public function validateCustomFields(array $fields, \CRM_Core_Form $form, array &$errors): bool + { $result = TRUE; $customFields = self::getCustomFields(); $customValues = []; @@ -130,36 +178,27 @@ abstract class BaseMailingList { if ($form instanceof \CRM_Group_Form_Edit) { $entityId = $form->getEntityId(); $type = self::GROUP_MAILING_LIST; - } - elseif ($form instanceof \CRM_Event_Form_ManageEvent_EventInfo) { + } elseif ($form instanceof \CRM_Event_Form_ManageEvent_EventInfo) { $entityId = $form->getEventID(); // It's things like this... $type = self::EVENT_MAILING_LIST; - } - else { + } else { throw new \Exception('Unknown form type'); } // Translate custom field names - foreach ($customFields as $customField) { - $fieldId = $customField['id']; - $fieldName = "custom_$fieldId"; - $getCustomField = function() use ($fields, $fieldName) { - foreach ($fields as $key => $value) { - if (preg_match("/^$fieldName" . "_.*/", $key)) { - return ['field_name' => $key, 'value' => $value]; - } - } - return NULL; - }; - $customValues[$customField['name']] = $getCustomField(); - } + $customValues = self::translateCustomFields($fields); // Validate e-mail address - if (!empty($customValues['E_mail_address'])) { + if (!empty($customValues['E_mail_address']['value'])) { if (!self::validateEmailAddress($customValues['E_mail_address']['value'])) { $errors[$customValues['E_mail_address']['field_name']] = E::ts('Invalid e-mail address'); $result = FALSE; } + if (!self::validateEmailDomain($customValues['E_mail_address']['value'])) { + $errors[$customValues['E_mail_address']['field_name']] = E::ts( + 'E-mail address does not match the configured domain'); + $result = FALSE; + } } // E-mail address must be unique for groups and events @@ -206,7 +245,8 @@ abstract class BaseMailingList { * @throws \CRM_Core_Exception * @throws \Civi\API\Exception\UnauthorizedException */ - static public function getByEmailAddress(string $emailAddress, int $excludeId = NULL): ?self { + static public function getByEmailAddress(string $emailAddress, int $excludeId = NULL): ?self + { $result = static::RELATED_CLASS::get() ->addSelect('*') ->addSelect('custom.*') @@ -236,20 +276,45 @@ abstract class BaseMailingList { protected abstract function setEntity(array $value): void; /** - * Check if the group is a mailing list. + * Check if this mailing list is enabled * * @return bool */ - public function isMailingList(): bool { - return (bool) $this->getEntity()[static::CUSTOM_GROUP_NAME . '.Enable_mailing_list']; + public function isEnabled(): bool + { + if (empty($this->getEntity())) { + return FALSE; + } + return (bool)$this->getEntity()[static::CUSTOM_GROUP_NAME . '.Enable_mailing_list']; } /** - * Check if the group is an AD mailing list. + * Check if this is a group. * * @return bool */ - public function isADGroup(): bool { + public function isGroup(): bool + { + return $this::RELATED_CLASS === \Civi\Api4\Group::class; + } + + /** + * Check if this is an event. + * + * @return bool + */ + public function isEvent(): bool + { + return $this::RELATED_CLASS === \Civi\Api4\Event::class; + } + + /** + * Check if this is an AD mailing list. + * + * @return bool + */ + public function isADGroup(): bool + { return FALSE; } @@ -258,7 +323,8 @@ abstract class BaseMailingList { * * @param array $values */ - protected function update(array $values): void { + protected function update(array $values): void + { $this->updateData += $values; } @@ -268,7 +334,8 @@ abstract class BaseMailingList { * @return void * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException */ - public function save(): void { + public function save(): void + { if (empty($this->updateData)) { return; } @@ -277,8 +344,7 @@ abstract class BaseMailingList { ->setValues($this->updateData) ->addWhere('id', '=', $this->getEntity()['id']) ->execute(); - } - catch (\Exception $e) { + } catch (\Exception $e) { $type = static::RELATED_TYPE; throw new MailinglistException( "Could not update $type with id '{$this->getEntity()['id']}'\n$e", @@ -292,7 +358,8 @@ abstract class BaseMailingList { * * @return string */ - public function getEmailAddress(): string { + public function getEmailAddress(): string + { return $this->getEntity()[static::CUSTOM_GROUP_NAME . '.E_mail_address']; } @@ -301,7 +368,8 @@ abstract class BaseMailingList { * * @return string */ - public function getTitle(): string { + public function getTitle(): string + { return $this->getEntity()['title']; } @@ -313,26 +381,24 @@ abstract class BaseMailingList { * @return void * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException */ - public function updateEmailAddress(string $emailAddress): void { + public function updateEmailAddress(string $emailAddress): void + { // Validate email address - if (ADGroupMailingList::validateEmailAddress($emailAddress)) { + if (!ADGroupMailingList::validateEmailAddress($emailAddress)) { throw new MailinglistException( - E::ts("Invalid e-mail address '%1'", [1 => $emailAddress]), + "Invalid e-mail address '$emailAddress'", MailinglistException::ERROR_CODE_INVALID_EMAIL_ADDRESS ); } - try { - static::update([ - static::CUSTOM_GROUP_NAME . '.E_mail_address' => $emailAddress, - ]); - } - catch (MailinglistException) { - $type = static::RELATED_TYPE; + if (!ADGroupMailingList::validateEmailDomain($emailAddress)) { throw new MailinglistException( - "Could not update e-mail address for $type with id '{$this->getEntity()['id']}'", - MailinglistException::ERROR_CODE_UPDATE_EMAIL_ADDRESS_FAILED + "E-mail address '$emailAddress' does not match the configured domain", + MailinglistException::ERROR_CODE_EMAIL_DOMAIN_MISMATCH ); } + static::update([ + static::CUSTOM_GROUP_NAME . '.E_mail_address' => $emailAddress, + ]); } /** @@ -343,7 +409,8 @@ abstract class BaseMailingList { * * @return string */ - public static function getLocationTypeName(): string { + public static function getLocationTypeName(): string + { return static::LOCATION_TYPE; } @@ -359,8 +426,9 @@ abstract class BaseMailingList { * * @return int */ - public function getId(): int { - return (int) $this->getEntity()['id']; + public function getId(): int + { + return (int)$this->getEntity()['id']; } /** @@ -372,36 +440,46 @@ abstract class BaseMailingList { * @param string|null $lastName * @param string|null $email * @param string|null $sid + * @param array|null $searchBy ['sid', 'email', 'names'] * * @return array|null ['contact' => $contact, 'found_by' => $found_by] * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException */ - protected function findExistingContact( + protected static function findExistingContact( string $firstName = NULL, string $lastName = NULL, string $email = NULL, string $sid = NULL, - ): ?array { + array $searchBy = NULL + ): ?array + { + // Set default search parameters. + if (empty($searchBy)) { + $searchBy = ['sid', 'email', 'names']; + } // Get the tags to restrict the search to. $tags = \Civi::settings()->get(E::SHORT_NAME . '_ad_contact_tags'); // Prepare the get call for reuse. - $prepareGetEmail = function () use ($tags,) { + $prepareGetEmail = function () use ($tags) { $selectFields = [ 'id', 'first_name', 'last_name', 'email.email', - GroupMailingList::CUSTOM_GROUP_NAME . '.Active_Directory_SID' + 'email.location_type_id.name', + 'Active_Directory.SID', ]; - $call = \Civi\Api4\Contact::get() + $call = \Civi\Api4\Contact::get() ->addSelect(...$selectFields) - ->addJoin('Email AS email', 'LEFT', ['id', '=', 'email.contact_id']); + ->addJoin('Email AS email', 'LEFT', + ['id', '=', 'email.contact_id']) + ->addGroupBy('id'); if ($tags) { - $call->addJoin('EntityTag AS entity_tag', 'LEFT', + $call->addJoin('EntityTag AS entity_tag', 'INNER', ['id', '=', 'entity_tag.entity_id'], - ['entity_tag.entity_table', '=', 'civicrm_contact'], + ['entity_tag.entity_table', '=', '"civicrm_contact"'], ['entity_tag.tag_id', 'IN', $tags] ); } @@ -409,61 +487,60 @@ abstract class BaseMailingList { }; // Try to find the contact by the SID. - try { - $contact = $prepareGetEmail() - ->addWhere(GroupMailingList::CUSTOM_GROUP_NAME . '.Active_Directory_SID', '=', $sid) - ->execute() - ->getArrayCopy(); - } - catch (\Exception $e) { - throw new MailinglistException( - "Could not get contact by SID '$sid': $e", - MailinglistException::ERROR_CODE_GET_CONTACT_FAILED, - ); - } - if (count($contact) > 1) { - throw new MailinglistException( - "Multiple contacts with the same SID '$sid' found", - MailinglistException::ERROR_CODE_MULTIPLE_CONTACTS_FOUND - ); - } - elseif (count($contact) === 1) { - return ['contact' => array_pop($contact), 'found_by' => 'sid']; + if (in_array('sid', $searchBy)) { + try { + $contact = $prepareGetEmail() + ->addWhere('Active_Directory.SID', '=', $sid) + ->execute() + ->getArrayCopy(); + } catch (\Exception $e) { + throw new MailinglistException( + "Could not get contact by SID '$sid': $e", + MailinglistException::ERROR_CODE_GET_CONTACT_FAILED, + ); + } + if (count($contact) > 1) { + throw new MailinglistException( + "Multiple contacts with the same SID '$sid' found", + MailinglistException::ERROR_CODE_MULTIPLE_CONTACTS_FOUND + ); + } elseif (count($contact) === 1) { + return ['contact' => array_pop($contact), 'found_by' => 'sid']; + } } // Try fo find the contact by the e-mail address. - try { - $contact = $prepareGetEmail() - ->addWhere('email', '=', $email) - ->execute() - ->getArrayCopy(); - } - catch (\Exception $e) { - throw new MailinglistException( - "Could not get contact by e-mail address '$email': $e", - MailinglistException::ERROR_CODE_GET_CONTACT_FAILED, - ); - } - if (count($contact) > 1) { - throw new MailinglistException( - "Multiple contacts with the same e-mail address '$email' found", - MailinglistException::ERROR_CODE_MULTIPLE_CONTACTS_FOUND - ); - } - elseif (count($contact) === 1) { - return ['contact' => array_pop($contact), 'found_by' => 'email']; + if (in_array('email', $searchBy)) { + try { + $contact = $prepareGetEmail() + ->addWhere('email.email', '=', $email) + ->execute() + ->getArrayCopy(); + } catch (\Exception $e) { + throw new MailinglistException( + "Could not get contact by e-mail address '$email': $e", + MailinglistException::ERROR_CODE_GET_CONTACT_FAILED, + ); + } + if (count($contact) > 1) { + throw new MailinglistException( + "Multiple contacts with the same e-mail address '$email' found", + MailinglistException::ERROR_CODE_MULTIPLE_CONTACTS_FOUND + ); + } elseif (count($contact) === 1) { + return ['contact' => array_pop($contact), 'found_by' => 'email']; + } } // Try to find the contact by the first and last name and only if the tags are set. - if ($tags) { + if (in_array('names', $searchBy) && $tags) { try { $contact = $prepareGetEmail() ->addWhere('first_name', '=', $firstName) ->addWhere('last_name', '=', $lastName) ->execute() ->getArrayCopy(); - } - catch (\Exception $e) { + } catch (\Exception $e) { throw new MailinglistException( "Could not get contact by first and last name '$firstName $lastName': $e", MailinglistException::ERROR_CODE_GET_CONTACT_FAILED, @@ -474,8 +551,7 @@ abstract class BaseMailingList { "Multiple contacts with the same first and last name found", MailinglistException::ERROR_CODE_MULTIPLE_CONTACTS_FOUND ); - } - elseif (count($contact) === 1) { + } elseif (count($contact) === 1) { return ['contact' => array_pop($contact), 'found_by' => 'names']; } } @@ -483,4 +559,157 @@ abstract class BaseMailingList { // If no contact was found, return NULL. return NULL; } + + /** + * Is this mailing list externally available? + * + * @return bool + */ + function isExternallyAvailable(): bool + { + return (bool)$this->getEntity()[static::CUSTOM_GROUP_NAME . '.Can_be_reached_externally']; + } + + /** + * Get the subject prefix. + * + * @return ?string + */ + function getSubjectPrefix(): ?string + { + return $this->getEntity()[static::CUSTOM_GROUP_NAME . '.Subject_prefix']; + } + + /** + * Synchronize this mailing list with mlmmj. + * + * @return array + * + */ + function sync(): array + { + $result = []; + $updateValues = []; + $mlmmjApi = MailingListApi::getInstance(); + + try { + // Get mailinglist + $mailinglist = $mlmmjApi->getMailingList($this->getEmailAddress()); + + // Create mailinglist if it does not exist yet + if (empty($mailinglist)) { + $mlmmjApi->createMailingList($this->getEmailAddress(), [ + 'close_list' => 'yes', + 'only_subscriber_can_post' => $this->isExternallyAvailable() ? 'no' : 'yes', + 'subject_prefix' => $this->getSubjectPrefix() ?? '', + ]); + $result['created'] = TRUE; + } // If the mailing list exists, check for updates + else { + $data = $mailinglist['_data']; + $externallyAvailable = $data['only_subscriber_can_post'] === 'no'; + $subjectPrefix = $data['subject_prefix']; + + // Identify changes + if ($externallyAvailable !== $this->isExternallyAvailable()) { + $updateValues['only_subscriber_can_post'] = $this->isExternallyAvailable() ? 'no' : 'yes'; + } + if ($subjectPrefix !== ($this->getSubjectPrefix() ?? '')) { + $updateValues['subject_prefix'] = $this->getSubjectPrefix() ?? ''; + } + + // Update the mlmmj mailing list + if (!empty($updateValues)) { + $mlmmjApi->updateMailingList( + $this->getEmailAddress(), + $updateValues, + ); + $result['updated'] = $updateValues; + } + } + + // Compare recipients + $civicrmRecipients = $this->getRecipients(); + $civicrmRecipientsEmails = array_map(function ($recipient) { + return $recipient->getEmail(); + }, $civicrmRecipients); + $mlmmjRecipients = $mlmmjApi->getMailingListSubscribers($this->getEmailAddress()); + + // Add and remove recipients + $recipientsToAdd = array_diff($civicrmRecipientsEmails, $mlmmjRecipients); + $recipientsToRemove = array_diff($mlmmjRecipients, $civicrmRecipientsEmails); + + // Add recipients + if (!empty($recipientsToAdd) || !empty($recipientsToRemove)) { + $mlmmjApi->updateSubscribers( + $this->getEmailAddress(), + $recipientsToAdd, + $recipientsToRemove, + ); + $result['recipients'] = [ + 'added' => array_values($recipientsToAdd), + 'removed' => array_values($recipientsToRemove), + ]; + } + + // Create contact activities + $added = array_filter($civicrmRecipients, function ($recipient) use ($recipientsToAdd) { + return in_array($recipient->getEmail(), $recipientsToAdd); + }); + $removed = array_filter($civicrmRecipients, function ($recipient) use ($recipientsToRemove) { + return in_array($recipient->getEmail(), $recipientsToRemove); + }); + + // Create activities for added contacts + foreach ($added as $recipient) { + /* @var MailingListRecipient $recipient */ + try { + MailingListRecipient::createSyncActivity( + $recipient->getContactId(), + 'Completed', + E::ts('Contact added to mailing list'), + E::ts("This contact has been added to the mailing list '%1'.", + [1 => $this->getEmailAddress()]) + ); + } catch (MailinglistException $e) { + \Civi::log(E::LONG_NAME)->error($e->getLogMessage()); + } + } + + // Create activities for removed contacts + foreach ($removed as $recipient) { + /* @var MailingListRecipient $recipient */ + 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->getEmailAddress()]) + ); + } catch (MailinglistException $e) { + \Civi::log(E::LONG_NAME)->error($e->getLogMessage()); + } + } + } catch (\Exception $e) { + \Civi::log(E::LONG_NAME)->error($e); + $result['is_error'] = 1; + $result['error'] = $e->getMessage(); + } + + return $result; + } + + /** + * Delete the corresponding mailing list on mlmmj and the email address. + * + * @return void + * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException + */ + function delete(): void + { + $mlmmjApi = MailingListApi::getInstance(); + $mlmmjApi->deleteMailingList($this->getEmailAddress()); + } + } diff --git a/Civi/Mailinglistsync/EventMailingList.php b/Civi/Mailinglistsync/EventMailingList.php index 9ac3d62..79898b2 100644 --- a/Civi/Mailinglistsync/EventMailingList.php +++ b/Civi/Mailinglistsync/EventMailingList.php @@ -1,7 +1,10 @@ event; + return $this->event ?? NULL; } /** @@ -37,20 +40,53 @@ class EventMailingList extends BaseMailingList { } public function getRecipients(): array { - // TODO: Implement getRecipients() method. + try { + $recipientData = Contact::get() + ->addSelect('id', 'first_name', 'last_name', 'email.email', 'Active_Directory.SID') + ->addJoin('Email AS email', 'LEFT', ['id', '=', 'email.contact_id']) + ->addJoin('Participant AS participant', 'INNER', + ['id', '=', 'participant.contact_id'], + ['participant.event_id', '=', $this->event['id']], + ['participant.status', 'IN', self::getEnabledParticipantStatus()], + ) + ->addGroupBy('id') + ->execute() + ->getArrayCopy(); + } + catch (\Exception $e) { + throw new MailinglistException( + "Could not get recipients for event with id '{$this->event['id']}': {$e->getMessage()}", + MailinglistException::ERROR_CODE_GET_RECIPIENTS_FAILED + ); + } + + $recipients = []; + foreach ($recipientData as $recipient) { + try { + $recipients[$recipient['email.email']] = new MailingListRecipient( + sid: $recipient['Active_Directory.SID'], + contact_id: $recipient['id'], + first_name: $recipient['first_name'], + last_name: $recipient['last_name'], + email: $recipient['email.email'], + ); + } catch (\Exception $e) { + throw new MailinglistException( + "Could not create recipient object for contact with id '{$recipient['id']}'\n$e", + MailinglistException::ERROR_CODE_GET_RECIPIENTS_FAILED + ); + } + } + + return $recipients; } /** * Get a list of participants status that are enabled for the mailing list. * - * @return array + * @return ?array */ - public static function getEnabledParticipantStatus(): array { + public static function getEnabledParticipantStatus(): ?array { return MailingListSettings::get(E::SHORT_NAME . '_participant_status'); } - - public static function create(array $values): BaseMailingList { - // TODO: Implement create() method. - } - } diff --git a/Civi/Mailinglistsync/Exceptions/ContactSyncException.php b/Civi/Mailinglistsync/Exceptions/ContactSyncException.php new file mode 100644 index 0000000..55e3582 --- /dev/null +++ b/Civi/Mailinglistsync/Exceptions/ContactSyncException.php @@ -0,0 +1,14 @@ +addValue('title', $title) - ->addValue('description', $description) - ->addValue(static::CUSTOM_GROUP_NAME . '.Email', $email) + ->addValue(static::CUSTOM_GROUP_NAME . '.E_mail_address', $email) + ->addValue(static::CUSTOM_GROUP_NAME . '.Enable_mailing_list', 1) ->addValue('is_active', 1); - // If the group is an AD group, add the AD SID - if (!empty($values['sid'])) { - $request->addValue(static::CUSTOM_GROUP_NAME . '.SID', $values['sid']); + // Add the description if it's not empty + if (!empty($description)) { + $request->addValue('description', $description); } - return new self($request->execute()->getArrayCopy()['id']); + + // If the group is an AD group, add the AD SID + if (!empty($sid)) { + $request->addValue(static::CUSTOM_GROUP_NAME . '.Active_Directory_SID', $sid); + } + $group = $request->execute()->first(); + return empty($sid) ? new self($group): new ADGroupMailingList($group); } catch (\Exception $e) { throw new MailinglistException( @@ -78,7 +85,7 @@ class GroupMailingList extends BaseMailingList { * @return bool */ public function isADGroup(): bool { - return !empty($this->group[static::CUSTOM_GROUP_NAME . '.Active_Directory_UUID']); + return !empty($this->group[static::CUSTOM_GROUP_NAME . '.Active_Directory_SID']); } /** @@ -91,11 +98,12 @@ class GroupMailingList extends BaseMailingList { try { $recipientData = Contact::get() ->addSelect('id', 'first_name', 'last_name', 'email.email', 'Active_Directory.SID') - ->addJoin('Email AS email', 'INNER', ['id', '=', 'email.contact_id']) - ->addJoin('GroupContact AS group_contact', 'INNER', ['id', '=', 'group_contact.contact_id']) - ->addJoin('LocationType AS location_type', 'INNER', ['email.location_type_id', '=', 'location_type.id']) - ->addWhere('group_contact.group_id', '=', $this->group['id']) - ->addWhere('location_type.name', '=', static::LOCATION_TYPE) + ->addJoin('Email AS email', 'LEFT', ['id', '=', 'email.contact_id']) + ->addJoin('GroupContact AS group_contact', 'INNER', + ['id', '=', 'group_contact.contact_id'], + ['group_contact.group_id', '=', $this->group['id']], + ['group_contact.status', '=', '"Added"'], + ) ->addGroupBy('id') ->execute() ->getArrayCopy(); @@ -109,13 +117,20 @@ class GroupMailingList extends BaseMailingList { $recipients = []; foreach ($recipientData as $recipient) { - $recipients[$recipient['email.email']] = new MailingListRecipient( - contact_id: $recipient['id'], - first_name: $recipient['first_name'], - last_name: $recipient['last_name'], - email: $recipient['email.email'], - sid: $recipient['Active_Directory.SID'], - ); + try { + $recipients[$recipient['email.email']] = new MailingListRecipient( + sid: $recipient['Active_Directory.SID'], + contact_id: $recipient['id'], + first_name: $recipient['first_name'], + last_name: $recipient['last_name'], + email: $recipient['email.email'], + ); + } catch (\Exception $e) { + throw new MailinglistException( + "Could not create recipient object for contact with id '{$recipient['id']}': {$e->getMessage()}", + MailinglistException::ERROR_CODE_GET_RECIPIENTS_FAILED + ); + } } return $recipients; @@ -139,7 +154,7 @@ class GroupMailingList extends BaseMailingList { * @return string */ public function getGroupDescription(): string { - return $this->group['description']; + return $this->group['description'] ?? ''; } /** @@ -148,10 +163,86 @@ class GroupMailingList extends BaseMailingList { * @param string $description * * @return void - * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException */ public function updateGroupDescription(string $description): void { $this->update(['description' => $description]); } + /** + * Add a contact to this mailing lists related group. + * + * @param int $contactId + * + * @return void + * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException + */ + protected function addContactToGroup(int $contactId): void { + try { + \Civi\Api4\GroupContact::create() + ->addValue('contact_id', $contactId) + ->addValue('group_id', $this->group['id']) + ->execute(); + } + catch (\Exception $e) { + if ($e->getMessage() === 'DB Error: already exists') { + $this->reAddContactToGroup($contactId); + } + else { + throw new MailinglistException( + "Could not add contact with id '$contactId' to group with id '{$this->group['id']}': $e", + MailinglistException::ERROR_CODE_ADD_CONTACT_TO_GROUP_FAILED + ); + } + } + } + + /** + * Remove a contact from this mailing lists related group. + * + * @param int $contactId + * + * @return void + * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException + */ + protected function removeContactFromGroup(int $contactId): void { + try { + \Civi\Api4\GroupContact::update() + ->addWhere('contact_id', '=', $contactId) + ->addWhere('group_id', '=', $this->group['id']) + ->addValue('status', 'Removed') + ->execute(); + } + catch (\Exception $e) { + throw new MailinglistException( + "Could not remove contact with id '$contactId' from group with id '{$this->group['id']}'\n$e", + MailinglistException::ERROR_CODE_REMOVE_CONTACT_FROM_GROUP_FAILED + ); + } + } + + /** + * If a group_contact entry already exists, but is marked as removed, + * re-add the contact to the group. + * + * @param int $contactId + * + * @return void + * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException + */ + private function reAddContactToGroup(int $contactId): void { + try { + \Civi\Api4\GroupContact::update() + ->addWhere('contact_id', '=', $contactId) + ->addWhere('group_id', '=', $this->group['id']) + ->addValue('status', 'Added') + ->execute(); + } + catch (\Exception $e) { + throw new MailinglistException( + "Could not re-add contact with id '$contactId' to group with id '{$this->group['id']}': $e", + MailinglistException::ERROR_CODE_ADD_CONTACT_TO_GROUP_FAILED + ); + } + } + } diff --git a/Civi/Mailinglistsync/MailingListApi.php b/Civi/Mailinglistsync/MailingListApi.php new file mode 100644 index 0000000..85aef45 --- /dev/null +++ b/Civi/Mailinglistsync/MailingListApi.php @@ -0,0 +1,321 @@ +mlmmjUrl = MailingListSettings::get('mlmmj_host'); + $this->mlmmjToken = MailingListSettings::get('mlmmj_token'); + $this->mlmmjPort = MailingListSettings::get('mlmmj_port'); + $this->dovecotUrl = MailingListSettings::get('dovecot_host'); + $this->dovecotToken = MailingListSettings::get('dovecot_token'); + $this->dovecotPort = MailingListSettings::get('dovecot_port'); + } + + /** + * Prepares a curl handle for the mlmmj API. + * + * @param null $path + * + * @return \CurlHandle + */ + private function prepareMlmmjCurl($path = NULL): \CurlHandle { + // Build URL + $url = $path + ? "{$this->mlmmjUrl}api/{$path}" + : "{$this->mlmmjUrl}api/"; + + // Set up curl handle + $curl = curl_init(); + + // Set options + curl_setopt_array($curl, [ + CURLOPT_URL => $url, + CURLOPT_PORT => $this->mlmmjPort, + CURLOPT_RETURNTRANSFER => TRUE, + CURLOPT_HTTPHEADER => [ + 'X-MLMMJADMIN-API-AUTH-TOKEN: ' . $this->mlmmjToken, + ], + ]); + + return $curl; + } + + /** + * Prepares a curl handle for the Dovecot API. + * + * @param $path + * + * @return \CurlHandle + */ + private function prepareDovecotCurl($path = NULL) { + // Build URL + $url = $path + ? "{$this->dovecotUrl}/{$path}" + : "{$this->dovecotUrl}"; + + // Set up curl handle + $curl = curl_init(); + + // Set options + curl_setopt_array($curl, [ + CURLOPT_URL => $url, + CURLOPT_PORT => $this->dovecotPort, + CURLOPT_RETURNTRANSFER => TRUE, + CURLOPT_HTTPHEADER => [ + 'Authorization: Bearer ' . $this->dovecotToken, + ] + ]); + + return $curl; + } + + /** + * Get a mailing list from mlmmj. + * + * @param string $mailinglistEmail + * + * @return array + * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException + */ + function getMailingList(string $mailinglistEmail): array { + $curl = $this->prepareMlmmjCurl($mailinglistEmail); + $result = json_decode(curl_exec($curl), TRUE); + + // Check result + if (curl_getinfo($curl, CURLINFO_HTTP_CODE) !== 200) { + throw new MailinglistException( + "Could not get mailinglist '$mailinglistEmail'", + MailinglistException::ERROR_CODE_GET_MAILINGLIST_FAILED, + ); + } + if ($result['_success'] === FALSE) { + // Return empty array if the account does not exist yet + if ($result['_msg'] === 'NO_SUCH_ACCOUNT') { + return []; + } + // Throw exception if the request failed + else { + throw new MailinglistException( + "Could not get mailinglist '$mailinglistEmail'", + MailinglistException::ERROR_CODE_GET_MAILINGLIST_FAILED, + ); + } + } + + return $result; + } + + /** + * Get the subscribers from mlmmj. + * + * @param string $mailinglistEmail + * + * @return array + * + * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException + */ + function getMailingListSubscribers(string $mailinglistEmail): array { + $curl = $this->prepareMlmmjCurl($mailinglistEmail . '/subscribers'); + $result = json_decode(curl_exec($curl), TRUE); + + // Check result + if (!$result['_success']) { + throw new MailinglistException( + "Could not get subscribers for mailinglist '$mailinglistEmail'", + MailinglistException::ERROR_CODE_GET_RECIPIENTS_FAILED, + ); + } + + return array_map(function($subscriber) { + return $subscriber['mail']; + }, $result['_data']); + } + + /** + * Create the email address via dovecot and the mailing list via mlmmj. + * + * @param string $mailinglistEmail + * @param array $options + * + * @return void + * + * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException + */ + function createMailingList(string $mailinglistEmail, array $options): void { + // Create email address via Dovecot API + $username = explode('@', $mailinglistEmail)[0]; + $dovecotCurl = $this->prepareDovecotCurl('list/' . $username); + curl_setopt($dovecotCurl, CURLOPT_CUSTOMREQUEST, 'PUT'); + $dovecutResult = json_decode(curl_exec($dovecotCurl), TRUE); + + // Check dovecot result (ignore 409 for already existing email addresses) + $statusCode = curl_getinfo($dovecotCurl, CURLINFO_HTTP_CODE); + if ($statusCode !== 201 && $statusCode !== 409) { + $message = "Could not create email address for '$mailinglistEmail'"; + if (!empty($dovecutResult['Message'])) { + $message .= ': ' . $dovecutResult['Message']; + } + throw new MailinglistException( + $message, + MailinglistException::ERROR_CODE_DOVECOT_CREATE_EMAIL_ADDRESS_FAILED, + ); + } + + // Create mailing list via mlmmj API + $mlmmjCurl = $this->prepareMlmmjCurl($mailinglistEmail); + curl_setopt($mlmmjCurl, CURLOPT_CUSTOMREQUEST, 'POST'); + curl_setopt($mlmmjCurl, CURLOPT_POSTFIELDS, http_build_query($options)); + $mlmmjResult = json_decode(curl_exec($mlmmjCurl), TRUE); + + // Check mlmmj result + if (!is_array($mlmmjResult) || $mlmmjResult['_success'] !== TRUE) { + $message = "Could not create mailinglist '$mailinglistEmail'"; + if (!empty($mlmmjResult['_msg'])) { + $message .= ': ' . $mlmmjResult['_msg']; + } + throw new MailinglistException( + $message, + MailinglistException::ERROR_CODE_CREATE_MAILING_LIST_FAILED, + ); + } + } + + /** + * Update a mailing list. + * + * @param string $mailinglistEmail + * @param array $options + * + * @return void + * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException + */ + function updateMailingList(string $mailinglistEmail, array $options): void { + $curl = $this->prepareMlmmjCurl($mailinglistEmail); + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'PUT'); + curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($options)); + $result = json_decode(curl_exec($curl), TRUE); + + // Check result + if (!empty($result['_success']) && $result['_success'] !== TRUE) { + $message = "Could not update mailinglist '$mailinglistEmail'"; + if (!empty($result['_msg'])) { + $message .= ': ' . $result['_msg']; + } + throw new MailinglistException( + $message, + MailinglistException::ERROR_CODE_UPDATE_MAILING_LIST_FAILED + ); + } + } + + /** + * Update the subscribers of a mailing list. + * + * @param string $mailinglistEmail + * @param ?array $recipientsToAdd + * @param ?array $recipientsToRemove + * + * @return void + * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException + */ + function updateSubscribers( + string $mailinglistEmail, + array $recipientsToAdd = NULL, + array $recipientsToRemove = NULL, + ): void { + $query = []; + if ($recipientsToAdd) { + $query['add_subscribers'] = implode(',', $recipientsToAdd); + } + if ($recipientsToRemove) { + $query['remove_subscribers'] = implode(',', $recipientsToRemove); + } + + $curl = $this->prepareMlmmjCurl($mailinglistEmail . '/subscribers'); + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST'); + curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($query)); + $result = json_decode(curl_exec($curl), TRUE); + + // Check result + if (!empty($result['_success']) && $result['_success'] !== TRUE) { + throw new MailinglistException( + "Could not update subscribers for mailinglist '$mailinglistEmail'", + MailinglistException::ERROR_CODE_UPDATE_SUBSCRIBERS_FAILED, + ); + } + } + + /** + * Delete a mailing list and its email address. + * + * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException + */ + function deleteMailingList(string $mailinglistEmail): void { + // Delete mlmmj mailing list + $mlmmjCurl = $this->prepareMlmmjCurl($mailinglistEmail); + curl_setopt($mlmmjCurl, CURLOPT_CUSTOMREQUEST, 'DELETE'); + $mlmmjResult = json_decode(curl_exec($mlmmjCurl), TRUE); + + // Check mlmmj result + if (!empty($mlmmjResult['_success']) && $mlmmjResult['_success'] !== TRUE) { + $message = "Could not delete mailinglist '$mailinglistEmail'"; + if (!empty($mlmmjResult['_msg'])) { + $message .= ': ' . $mlmmjResult['_msg']; + } + throw new MailinglistException( + $message, + MailinglistException::ERROR_CODE_DELETE_MAILING_LIST_FAILED, + ); + } + + // Delete dovecot email address + $username = explode('@', $mailinglistEmail)[0]; + $dovecotCurl = $this->prepareDovecotCurl('list/' . $username); + curl_setopt($dovecotCurl, CURLOPT_CUSTOMREQUEST, 'DELETE'); + $dovecotResult = json_decode(curl_exec($dovecotCurl), TRUE); + + // Check dovecot result (ignore 404 for non-existing email addresses) + $statusCode = curl_getinfo($dovecotCurl, CURLINFO_HTTP_CODE); + if ($statusCode === 404) { + $message = "Email '$mailinglistEmail' does not exist in Dovecot"; + throw new MailinglistException( + $message, + MailinglistException::ERROR_CODE_DELETE_EMAIL_ADDRESS_FAILED, + ); + } + if ($statusCode !== 200) { + $message = "Could not delete email address for '$mailinglistEmail'"; + if (!empty($dovecotResult['Message'])) { + $message .= ': ' . $dovecotResult['Message']; + } + throw new MailinglistException( + $message, + MailinglistException::ERROR_CODE_DOVECOT_CREATE_EMAIL_ADDRESS_FAILED, + ); + } + } +} diff --git a/Civi/Mailinglistsync/MailingListManager.php b/Civi/Mailinglistsync/MailingListManager.php index e52b749..bfa2a6a 100644 --- a/Civi/Mailinglistsync/MailingListManager.php +++ b/Civi/Mailinglistsync/MailingListManager.php @@ -1,10 +1,13 @@ enabled = $settings[E::SHORT_NAME . '_enable'] ?? FALSE; @@ -82,17 +85,6 @@ class MailingListManager { $this->dovecotToken = $settings[E::SHORT_NAME . '_dovecot_token'] ?? throw new \Exception('No dovecot token set'); $this->dovecotPort = $settings[E::SHORT_NAME . '_dovecot_port'] ?? 443; + $this->mlmmjApi = MailingListApi::getInstance(); } - - function createEmailAddress(string $emailAddress) {} // TODO - - function deleteEmailAddress(string $emailAddress) {} // TODO - - function createMailingList(BaseMailingList $mailingList) {} // TODO - - function deleteMailingList(BaseMailingList $mailingList) {} // TODO - - function updateMailingList(BaseMailingList $mailingList) {} // TODO - - } diff --git a/Civi/Mailinglistsync/MailingListRecipient.php b/Civi/Mailinglistsync/MailingListRecipient.php index dbd28b8..49273e6 100644 --- a/Civi/Mailinglistsync/MailingListRecipient.php +++ b/Civi/Mailinglistsync/MailingListRecipient.php @@ -1,44 +1,41 @@ contactId = $contact_id; $this->firstName = $first_name; @@ -48,83 +45,12 @@ class MailingListRecipient { $this->updateData = []; } -// /** -// * Get or create a contact. -// * -// * @param string $fistName -// * @param string $lastName -// * @param string $email -// * @param string $locationType The location type of the email address -// * -// * @return \Civi\Mailinglistsync\MailingListRecipient -// * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException -// */ -// public static function getOrCreate( -// string $fistName, -// string $lastName, -// string $email, -// string $locationType, -// ): self { -// -// // Verify location type -// $locationTypes = [ -// GroupMailingList::LOCATION_TYPE, -// ADGroupMailingList::LOCATION_TYPE, -// EventMailingList::LOCATION_TYPE, -// ]; -// if (!in_array($locationType, $locationTypes)) { -// throw new MailinglistException( -// E::ts('Invalid location type'), -// MailinglistException::ERROR_CODE_INVALID_LOCATION_TYPE, -// ); -// } -// -// // Try to get contact -// try { -// $contact = \Civi\Api4\Contact::get() -// ->addSelect('*', 'location_type.*') -// ->addJoin('Email AS email', 'LEFT', ['id', '=', 'email.contact_id']) -// ->addJoin('LocationType AS location_type', 'LEFT', ['email.location_type_id', '=', 'location_type.id']) -// ->addWhere('email.email', '=', $email) -// ->addWhere('location_type.name', '=', $locationType) -// ->execute() -// ->first(); -// } -// catch (\Exception $e) { -// throw new MailinglistException( -// E::ts('Failed to get contact'), -// MailinglistException::ERROR_CODE_GET_CONTACT_FAILED, -// ); -// } -// -// // Create contact if it does not exist -// if (empty($contact)) { -// try { -// $contact = \Civi\Api4\Contact::create(FALSE) -// ->addValue('contact_type', 'Individual') -// ->addValue('first_name', $fistName) -// ->addValue('last_name', $lastName) -// ->setChain([ -// 'Email.create', -// \Civi\Api4\Email::create(FALSE) -// ->addValue('contact_id', '$id') -// ->addValue('email', $email) -// ->addValue('location_type_id.name', $locationType), -// ]) -// ->execute() -// ->first(); -// array_pop($contact['Email.create']); -// } -// catch (\Exception $e) { -// throw new MailinglistException( -// E::ts('Failed to create contact'), -// MailinglistException::ERROR_CODE_CREATE_CONTACT_FAILED, -// ); -// } -// } -// -// return new static(contact: $contact, email: $email); -// } + /** + * Data to be updated. Use the `save()` method to apply the update. + * + * @var array + */ + private array $updateData; /** * Create a new contact and return the recipient. @@ -133,68 +59,133 @@ class MailingListRecipient { * @param string $lastName * @param string $email * @param string $sid + * @param string $locationType * * @return \Civi\Mailinglistsync\MailingListRecipient * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException */ public static function createContact( - string $firstName, - string $lastName, string $email, string $sid, + string $locationType, + string $firstName = '', + string $lastName = '', ): self { // Try to create a new contact try { $contact = \Civi\Api4\Contact::create(FALSE) ->addValue('contact_type', 'Individual') - ->addValue('first_name', $firstName) - ->addValue('last_name', $lastName) - ->setChain([ + ->addValue('Active_Directory.SID', $sid) + ->addChain( 'Email.create', \Civi\Api4\Email::create(FALSE) + ->addValue('contact_id', '$id') ->addValue('email', $email) - ->addValue('location_type_id.name', 'Main'), - ]) - ->execute(); + ->addValue('location_type_id:name', $locationType), + ); + + // If contact has a firstname, add it to the chain + if (!empty($firstName)) { + $contact->addValue('first_name', $firstName); + } + + // If contact has a lastname, add it to the chain + if (!empty($lastName)) { + $contact->addValue('last_name', $lastName); + } + + // If contact has no firstname or lastname, add email as display name + if (empty($firstName) && empty($lastName)) { + $contact->addValue('display_name', $email); + } + + // Execute the query + $contact->execute(); } catch (\Exception $e) { throw new MailinglistException( - E::ts("Failed to create contact: {$e->getMessage()}"), + "Failed to create contact: {$e->getMessage()}", MailinglistException::ERROR_CODE_CREATE_CONTACT_FAILED, ); } return new static( + sid: $sid, contact_id: $contact['id'], first_name: $firstName, last_name: $lastName, email: $email, - sid: $sid, ); } /** - * Get a list of group mailing lists the contact is subscribed to. + * Get a recipient by its email address. * - * @return array + * @param string $email + * + * @return MailingListRecipient * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException */ - public function getMailingLists(): array { + public static function getContactIdEmail(string $email): MailingListRecipient { + try { + $recipientData = Contact::get() + ->addSelect('id', 'first_name', 'last_name', 'email.email', 'Active_Directory.SID') + ->addJoin('Email AS email', 'LEFT', ['id', '=', 'email.contact_id']) + ->addWhere('email.email', '=', $email) + ->addGroupBy('id') + ->execute() + ->first(); + } + catch (\Exception $e) { + throw new MailinglistException( + "Could not get recipient by email '{$email}': {$e->getMessage()}", + MailinglistException::ERROR_CODE_GET_RECIPIENTS_FAILED + ); + } + + try { + $recipient = new MailingListRecipient( + sid: $recipientData['Active_Directory.SID'], + contact_id: $recipientData['id'], + first_name: $recipientData['first_name'], + last_name: $recipientData['last_name'], + email: $recipientData['email.email'], + ); + } catch (\Exception $e) { + throw new MailinglistException( + "Could not create recipient object for contact with id '{$recipientData['id']}': {$e->getMessage()}", + MailinglistException::ERROR_CODE_GET_RECIPIENTS_FAILED + ); + } + + return $recipient; + } + + /** + * Get a list of group mailing lists the contact is subscribed to. + * + * @return ?array + * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException + */ + public function getMailingLists(): ?array { $mailingLists = []; try { $groups = \Civi\Api4\GroupContact::get() ->addSelect('group_id') + ->addJoin( + 'Group AS group', 'INNER', + ['group_id', '=', 'group.id'], + ['group.' . GroupMailingList::CUSTOM_GROUP_NAME . '.Enable_mailing_list', '=', 1], + ) ->addWhere('contact_id', '=', $this->getContactId()) ->addWhere('status', '=', 'Added') - ->addWhere(GroupMailingList::CUSTOM_GROUP_NAME . '.Enable_mailing_list', '=', TRUE) - ->addWhere(GroupMailingList::CUSTOM_GROUP_NAME . '.Active_Directory_SID', 'IS EMPTY') ->execute() ->getArrayCopy(); } catch (\Exception $e) { throw new MailinglistException( - E::ts('Failed to get group mailing lists'), + 'Failed to get group mailing lists', MailinglistException::ERROR_CODE_GET_GROUP_MAILING_LISTS_FAILED, ); } @@ -213,100 +204,164 @@ class MailingListRecipient { /** * Get a list of AD group mailing lists the contact is subscribed to. * - * @return array + * @return ?array * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException */ - public function getAdMailingLists(): array { + public function getAdMailingLists(): ?array { $mailingLists = []; try { $groups = \Civi\Api4\GroupContact::get() ->addSelect('group_id') + ->addJoin( + 'Group AS group', 'INNER', + ['group_id', '=', 'group.id'], + ['group.' . GroupMailingList::CUSTOM_GROUP_NAME . '.Enable_mailing_list', '=', 1], + ['group.' . GroupMailingList::CUSTOM_GROUP_NAME . '.Active_Directory_SID', 'IS NOT EMPTY'], + ) ->addWhere('contact_id', '=', $this->getContactId()) ->addWhere('status', '=', 'Added') - ->addWhere(GroupMailingList::CUSTOM_GROUP_NAME . '.Enable_mailing_list', '=', TRUE) - ->addWhere(GroupMailingList::CUSTOM_GROUP_NAME . '.Active_Directory_SID', 'IS NOT EMPTY') ->execute() ->getArrayCopy(); } catch (\Exception $e) { throw new MailinglistException( - E::ts('Failed to get AD group mailing lists'), + 'Failed to get AD group mailing lists', MailinglistException::ERROR_CODE_GET_AD_GROUP_MAILING_LISTS_FAILED, ); } foreach ($groups as $group) { - $mailingList = new GroupMailingList($group['group_id']); - if ($mailingList->isADGroup()) { - $mailingList = new ADGroupMailingList($group['group_id']); ; - } - $mailingLists[] = $mailingList; + $mailingLists[] = new ADGroupMailingList($group['group_id']); } return $mailingLists; } /** + * Get a list of event mailing lists the contact is subscribed to. + * + * @return ?array * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException */ - public function getEventMailingLists(): array { + public function getEventMailingLists(): ?array { $mailingLists = []; try { $groups = \Civi\Api4\Participant::get() ->addSelect('event_id') + ->addJoin( + 'Event AS event', 'INNER', + ['event_id', '=', 'event.id'], + ['event.' . EventMailingList::CUSTOM_GROUP_NAME . '.Enable_mailing_list', '=', 1], + ) ->addWhere('contact_id', '=', $this->getContactId()) ->addWhere('status_id', 'IN', EventMailingList::getEnabledParticipantStatus()) - ->addWhere(GroupMailingList::CUSTOM_GROUP_NAME . '.Enable_mailing_list', '=', TRUE) ->execute() ->getArrayCopy(); } catch (\Exception $e) { throw new MailinglistException( - E::ts('Failed to get event mailing lists'), + 'Failed to get event mailing lists', MailinglistException::ERROR_CODE_GET_EVENT_MAILING_LISTS_FAILED, ); } + foreach ($groups as $group) { - $mailingList = new EventMailingList($group['group_id']); - $mailingLists[] = $mailingList; + $mailingLists[] = new EventMailingList($group['group_id']); } return $mailingLists; } - public function getContactId(): int { + /** + * Get the contact ID. + * + * @return int|NULL + */ + public function getContactId(): ?int { return $this->contactId; } - public function getFirstName(): string { + /** + * Get the first name of the contact. + * + * @return string|NULL + */ + public function getFirstName(): ?string { return $this->firstName; } - public function getLastName(): string { + /** + * Get the last name of the contact. + * + * @return string|NULL + */ + public function getLastName(): ?string { return $this->lastName; } - public function getEmail(): string { + /** + * Get the email address of the contact. + * + * @return string|NULL + */ + public function getEmail(): ?string { return $this->email; } - public function getSid(): string { + /** + * Get the SID of the contact. + * + * @return string|NULL + */ + public function getSid(): ?string { return $this->sid; } - public function setFirstName(string $firstName): void { + /** + * Update the first name of the contact. + * + * @param string $firstName + * + * @return void + */ + public function updateFirstName(string $firstName): void { $this->updateData += ['first_name' => $firstName]; } - public function setLastName(string $lastName): void { + /** + * Update the last name of the contact. + * + * @param string $lastName + * + * @return void + */ + public function updateLastName(string $lastName): void { $this->updateData += ['last_name' => $lastName]; } - public function setEmail(string $email): void { + /** + * Update the email address of the contact. + * + * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException + */ + public function updateEmail(string $email): void { + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + throw new MailinglistException( + "Invalid email address '$email'", + MailinglistException::ERROR_CODE_INVALID_EMAIL_ADDRESS, + ); + } $this->updateData += ['email' => $email]; } - public function setSid(string $sid): void { - $this->updateData += ['sid' => $sid]; + /** + * Update the SID of the contact. + * + * @param string $sid + * + * @return void + */ + public function updateSid(string $sid): void { + $this->updateData += [self::CUSTOM_GROUP_NAME . '.SID' => $sid]; } /** @@ -319,17 +374,156 @@ class MailingListRecipient { return; } + $contactValues = ['first_name', 'last_name', self::CUSTOM_GROUP_NAME . '.SID']; + $contactUpdates = array_intersect_key($this->updateData, array_flip($contactValues)); + + if (!empty($contactUpdates)) { + try { + \Civi\Api4\Contact::update() + ->setValues($contactUpdates) + ->addWhere('id', '=', $this->getContactId()) + ->execute(); + \Civi::log()->debug( + "Updated contact '{$this->getContactId()}' with data: " . json_encode($this->updateData) + ); + } + catch (\Exception $e) { + \Civi::log()->error( + "Failed to update contact '{$this->getContactId()}': $e" + ); + throw new MailinglistException( + "Failed to update contact: {$e->getMessage()}", + MailinglistException::ERROR_CODE_UPDATE_ENTITY_FAILED, + ); + } + } + + // Update email address + if (!empty($this->updateData['email'])) { + try { + \Civi\Api4\Email::update() + ->addValue('email', $this->updateData['email']) + ->addWhere('contact_id', '=', $this->getContactId()) + ->execute(); + \Civi::log()->debug( + "Updated email address for contact '{$this->getContactId()}'" + ); + } + catch (\Exception $e) { + \Civi::log()->error( + "Failed to update email address for contact '{$this->getContactId()}': $e" + ); + throw new MailinglistException( + "Failed to update email address: {$e->getMessage()}", + MailinglistException::ERROR_CODE_UPDATE_ENTITY_FAILED, + ); + } + } + try { - \Civi\Api4\Contact::update() - ->setValues($this->updateData) - ->addWhere('id', '=', $this->getContactId()) - ->execute(); - } catch (\Exception $e) { + self::createSyncActivity( + $this->getContactId(), + 'Completed', + E::ts('Contact updated'), + E::ts('The contact data was updated by the mailing list synchronization: %1', [ + 1 => json_encode($this->updateData) + ]) + ); + } + catch (\Exception $e) { + \Civi::log()->error( + "Failed to create activity for contact '{$this->getContactId()}': $e" + ); throw new MailinglistException( - E::ts("Failed to update contact: {$e->getMessage()}"), - MailinglistException::ERROR_CODE_UPDATE_ENTITY_FAILED, + "Failed to create activity for contact '{$this->getContactId()}': $e", + MailinglistException::ERROR_CODE_CREATE_ACTIVITY_FAILED, ); } } + /** + * Get a list of email location types for the contact. + * + * @return array + * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException + */ + public function getEmailLocationTypes(): array { + try { + $result = \Civi\Api4\Email::get() + ->addSelect('location_type_id:name') + ->addWhere('contact_id', '=', $this->getContactId()) + ->execute() + ->getArrayCopy(); + return array_column($result, 'location_type_id:name'); + } + catch (\Exception $e) { + throw new MailinglistException( + 'Failed to get email location types', + MailinglistException::ERROR_CODE_GET_EMAIL_LOCATION_TYPES_FAILED, + ); + } + } + + /** + * Create an e-mail address for the contact. + * + * @param string $email + * @param string $locationType + * + * @return array|NULL + * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException + */ + public function createEmail(string $email, string $locationType): ?array { + try { + $result = \Civi\Api4\Email::create(FALSE) + ->addValue('contact_id', $this->getContactId()) + ->addValue('email', $email) + ->addValue('location_type_id:name', $locationType) + ->execute() + ->first(); + } + catch (\Exception $e) { + throw new MailinglistException( + 'Failed to create email', + MailinglistException::ERROR_CODE_CREATE_EMAIL_ADDRESS_FAILED, + ); + } + $this->email = $email; + return $result; + } + + /** + * Add a synchronization activity to a contact. + * + * @param int $targetContactId + * @param string $status + * @param string $subject + * @param string $details + * + * @return void + * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException + */ + public static function createSyncActivity( + int $targetContactId, + string $status, + string $subject, + string $details, + ): void { + try { + \Civi\Api4\Activity::create(FALSE) + ->addValue('source_contact_id', getSessionUser()) + ->addValue('activity_type_id:name', 'Mailing_List_Synchronization_Activity') + ->addValue('subject', $subject) + ->addValue('details', $details) + ->addValue('status_id:name', ucfirst($status)) + ->addValue('target_contact_id', $targetContactId) + ->execute()->first(); + } + catch (\Exception $e) { + throw new MailinglistException( + "Failed to create contact activity: $e", + MailinglistException::ERROR_CODE_CREATE_ACTIVITY_FAILED, + ); + } + } } diff --git a/Civi/Mailinglistsync/MailingListSettings.php b/Civi/Mailinglistsync/MailingListSettings.php index be2a8eb..7dc1e1b 100644 --- a/Civi/Mailinglistsync/MailingListSettings.php +++ b/Civi/Mailinglistsync/MailingListSettings.php @@ -1,8 +1,8 @@ [ 'data_type' => 'boolean', ], + E::SHORT_NAME . '_domain' => [ + 'data_type' => 'string', + ], E::SHORT_NAME . '_mlmmj_host' => [ 'data_type' => 'string', ], @@ -75,7 +78,7 @@ class MailingListSettings { */ public static function get($key = NULL): mixed { if (!is_null($key)) { - return \Civi::settings()->get($key); + return \Civi::settings()->get(E::SHORT_NAME . '_' . $key); } else { $settings = []; @@ -95,30 +98,36 @@ class MailingListSettings { * @return void */ public static function validate(array $values, array &$errors): void { + + // Validate domain + if (empty($values[E::SHORT_NAME . '_domain'])) { + $errors[E::SHORT_NAME . '_domain'] = E::ts('Domain is required'); + } + // Validate url if synchronization is enabled - $url = $values[E::SHORT_NAME . '_mailinglist_mlmmj_host']; - if (!empty($values[E::SHORT_NAME . '_mailinglist_mlmmj_enable']) && !filter_var($url, FILTER_VALIDATE_URL)) { - $errors[E::SHORT_NAME . '_mailinglist_mlmmj_host'] = E::ts('Invalid URL'); + $url = $values[E::SHORT_NAME . '_mlmmj_host']; + if (!empty($values[E::SHORT_NAME . '__mlmmj_enable']) && !filter_var($url, FILTER_VALIDATE_URL)) { + $errors[E::SHORT_NAME . '_mlmmj_host'] = E::ts('Invalid URL'); } // Validate port if synchronization is enabled and port is set - $port = $values[E::SHORT_NAME . '_mailinglist_mlmmj_port'] ?? NULL; - if (!empty($values[E::SHORT_NAME . '_mailinglist_mlmmj_enable']) && !empty($port)) { + $port = $values[E::SHORT_NAME . '_mlmmj_port'] ?? NULL; + if (!empty($values[E::SHORT_NAME . '_mlmmj_enable']) && !empty($port)) { if (is_numeric($port)) { - $errors[E::SHORT_NAME . '_mailinglist_mlmmj_port'] = E::ts('Port must be a number'); + $errors[E::SHORT_NAME . '_mlmmj_port'] = E::ts('Port must be a number'); } if ($port < 1 || $port > 65535) { - $errors[E::SHORT_NAME . '_mailinglist_mlmmj_port'] = E::ts('Port must be between 1 and 65535'); + $errors[E::SHORT_NAME . '_mlmmj_port'] = E::ts('Port must be between 1 and 65535'); } } // Require host and token if mlmmj is enabled - if (!empty($values[E::SHORT_NAME . '_mailinglist_mlmmj_enable'])) { - if (empty($values[E::SHORT_NAME . '_mailinglist_mlmmj_host'])) { - $errors[E::SHORT_NAME . '_mailinglist_mlmmj_host'] = E::ts('Host is required'); + if (!empty($values[E::SHORT_NAME . '_mlmmj_enable'])) { + if (empty($values[E::SHORT_NAME . '_mlmmj_host'])) { + $errors[E::SHORT_NAME . '_mlmmj_host'] = E::ts('Host is required'); } - if (empty($values[E::SHORT_NAME . '_mailinglist_mlmmj_token'])) { - $errors[E::SHORT_NAME . '_mailinglist_mlmmj_token'] = E::ts('Token is required'); + if (empty($values[E::SHORT_NAME . '_mlmmj_token'])) { + $errors[E::SHORT_NAME . '_mlmmj_token'] = E::ts('Token is required'); } } } diff --git a/Civi/Mailinglistsync/QueueHelper.php b/Civi/Mailinglistsync/QueueHelper.php index 89692e4..3988dbc 100644 --- a/Civi/Mailinglistsync/QueueHelper.php +++ b/Civi/Mailinglistsync/QueueHelper.php @@ -1,20 +1,13 @@ 'drop', ]); $this->eventQueue = \Civi::queue( - 'propeace-mailinglist-group-queue', [ + 'propeace-mailinglist-event-queue', [ 'type' => 'SqlParallel', 'is_autorun' => FALSE, 'reset' => FALSE, 'error' => 'drop', ]); $this->emailQueue = \Civi::queue( - 'propeace-mailinglist-group-queue', [ + 'propeace-mailinglist-email-queue', [ 'type' => 'SqlParallel', 'is_autorun' => FALSE, 'reset' => FALSE, 'error' => 'drop', ]); - } - /** - * Protect singleton from being cloned. - */ - protected function __clone() { } - - /** - * Protect unserialize method to prevent cloning of the instance. - * @throws \Exception - */ - public function __wakeup() - { - throw new MailinglistSyncException( - "Cannot unserialize a singleton.", - MailinglistSyncException::ERROR_CODE_UNSERIALIZE_SINGLETON - ); - } - - /** - * Returns the singleton instance. - * - * @return \Civi\Mailinglistsync\QueueHelper - */ - public static function getInstance(): QueueHelper - { - $cls = static::class; - if (!isset(self::$instances[$cls])) { - self::$instances[$cls] = new static(); - \Civi::log()->debug(E::LONG_NAME . ": Created new instance of $cls"); - } - - return self::$instances[$cls]; + $this->groups = []; + $this->events = []; + $this->emails = []; } /** @@ -138,17 +102,31 @@ class QueueHelper { $this->emails[] = $email; } + /** + * Stores an email address in the queue helper singleton. + * + * @param \CRM_Queue_TaskContext $context + * @param string $email + * + * @return bool + */ + public static function storeEmail(\CRM_Queue_TaskContext $context, string $email): bool { + self::getInstance()->addToEmails($email); + return TRUE; + } + /** * Stores an instance of the given class in the queue helper singleton. * Meant to be passed as callback to the queue. * + * @param \CRM_Queue_TaskContext $context * @param int $entityId * @param string $class * - * @return void + * @return bool TRUE if the instance was stored successfully. * @throws \Civi\Mailinglistsync\Exceptions\MailinglistException */ - public static function storeInstance(int $entityId, string $class): void { + public static function storeInstance(\CRM_Queue_TaskContext $context, int $entityId, string $class): bool { // Throw exception if class is invalid if ($class != GroupMailingList::class && @@ -161,17 +139,23 @@ class QueueHelper { } // Instantiate the mailing list object - $instance = new $class(); - $instance->load($entityId); + /* @var $instance GroupMailingList|ADGroupMailingList|EventMailingList */ + $instance = new $class($entityId); + + // Ignore disabled mailing lists + if (!$instance->isEnabled()) { + return TRUE; + } // Store instance in the queue helper singleton - match ($class) { - GroupMailingList::class, ADGroupMailingList::class => self::getInstance()->addToGroups($instance), - EventMailingList::class => self::getInstance()->addToEvents($instance), - default => throw new MailinglistException( - "Invalid class '$class'", - MailinglistException::ERROR_CODE_INVALID_CLASS - ), - }; + if ( + $instance::class === GroupMailingList::class + || $instance::class === ADGroupMailingList::class + ) { + self::getInstance()->addToGroups($instance); + } else { + self::getInstance()->addToEvents($instance); + } + return TRUE; } } diff --git a/Civi/Mailinglistsync/Singleton.php b/Civi/Mailinglistsync/Singleton.php new file mode 100644 index 0000000..c962569 --- /dev/null +++ b/Civi/Mailinglistsync/Singleton.php @@ -0,0 +1,50 @@ +getMessage()}", MailinglistException::ERROR_CODE_GET_LOCATION_TYPES_FAILED, ); } @@ -47,3 +48,13 @@ function getLocationTypes(): array { return $locationTypes; } + +/** + * Get a user ID of the current session. + * + * @return int + */ +function getSessionUser(): int { + $session = \CRM_Core_Session::singleton(); + return (int) $session->get('userID'); +} diff --git a/api/v3/Mailinglistsync/Adgroupsync.php b/api/v3/Mailinglistsync/Adgroupsync.php index 09442af..fcb75ae 100644 --- a/api/v3/Mailinglistsync/Adgroupsync.php +++ b/api/v3/Mailinglistsync/Adgroupsync.php @@ -2,6 +2,7 @@ use Civi\Mailinglistsync\ADGroupMailingList; use Civi\Mailinglistsync\Exceptions\MailinglistException; +use Civi\Mailinglistsync\MailingListSettings; use CRM_Mailinglistsync_ExtensionUtil as E; /** @@ -14,31 +15,31 @@ use CRM_Mailinglistsync_ExtensionUtil as E; */ function _civicrm_api3_mailinglistsync_Adgroupsync_spec(&$spec) { $spec['sid'] = [ - 'api.required' => 1, + 'api.required' => 0, 'type' => CRM_Utils_Type::T_STRING, 'title' => E::ts('Active Directory SID'), 'description' => E::ts('The Active Directory SID of the group'), ]; $spec['email'] = [ - 'api.required' => 1, + 'api.required' => 0, 'type' => CRM_Utils_Type::T_EMAIL, 'title' => 'Email Address', 'description' => 'Email address of the mailing list', ]; $spec['recipients'] = [ - 'api.required' => 1, + 'api.required' => 0, 'type' => CRM_Utils_Type::T_STRING, 'title' => E::ts('Recipients'), 'description' => E::ts('Array of email addresses and SIDs'), ]; $spec['name'] = [ - 'api.required' => 1, + 'api.required' => 0, 'type' => CRM_Utils_Type::T_STRING, 'title' => E::ts('Mail List Name'), 'description' => E::ts('Name of the mailing list'), ]; $spec['description'] = [ - 'api.required' => 1, + 'api.required' => 0, 'type' => CRM_Utils_Type::T_LONGTEXT, 'title' => E::ts('Mail List Description'), 'description' => E::ts('Description of the mailing list'), @@ -58,123 +59,180 @@ function _civicrm_api3_mailinglistsync_Adgroupsync_spec(&$spec) { * @see civicrm_api3_create_success */ function civicrm_api3_mailinglistsync_Adgroupsync($params) { - // Filter illegal params $allowed_params = []; _civicrm_api3_mailinglistsync_Adgroupsync_spec($allowed_params); $params = array_intersect_key($params, $allowed_params); - // Prepare result array - $result = [ - 'sid' => $params['sid'], - 'email' => $params['email'], - 'name' => $params['name'], - 'description' => $params['description'], - 'group_created' => FALSE, - 'group_updated' => FALSE, - ]; + // Check if the mailing list sync is enabled + $enabled = (bool) MailingListSettings::get('enable'); + if (!$enabled) { + return civicrm_api3_create_success( + ['message' => 'Mailing list sync is disabled'], [], 'Mailinglist', 'Adgroupsync' + ); + } - try { - // Try to get mailing list by SID - $adGroupMailingList = ADGroupMailingList::getBySID($params['sid']); + // Validate params - // If no AD group mailing list found, create new - if (!$adGroupMailingList) { + // Decode recipients JSON if it's a string + if (!empty($params['recipients'])) { + if (is_string($params['recipients'])) { try { - $adGroupMailingList = ADGroupMailingList::createGroup( - title: $params['name'], - description: $params['description'], - email: $params['email'], - sid: $params['sid'], - ); - $result['group_created']['group_id'] = $adGroupMailingList->getId(); - $result['group_created']['is_error'] = FALSE; + $params['recipients'] = json_decode($params['recipients'], TRUE); } - catch (MailinglistException $e) { - \Civi::log(E::LONG_NAME)->error($e->getLogMessage()); - $result['group_created']['is_error'] = TRUE; + catch (Exception $e) { + return civicrm_api3_create_error( + 'Failed to decode recipients JSON', + [ + 'params' => $params, + 'entity' => 'Mailinglistsync', + 'action' => 'Adgroupsync', + ]); } } + // Throw error if recipients is not an array + if (!is_array($params['recipients'])) { + return civicrm_api3_create_error( + 'Recipients must be an array', + [ + 'params' => $params, + 'entity' => 'Mailinglistsync', + 'action' => 'Adgroupsync', + ]); + } + } - // Sync AD group mailing list values - else { - if ($adGroupMailingList->getTitle() !== $params['name']) { - try { - $adGroupMailingList->updateGroupTitle($params['name']); - \Civi::log(E::LONG_NAME)->info( - "Updated group '{$adGroupMailingList->getId()}' title from '{$adGroupMailingList->getTitle()}' to '{$params['name']}'" - ); - $result['group_updated']['title'] = [ - 'is_error' => FALSE, - 'old' => $adGroupMailingList->getTitle(), - 'new' => $params['name'], - ]; - } - catch (MailinglistException $e) { - \Civi::log(E::LONG_NAME)->error($e->getLogMessage()); - $result['group_updated']['title'] = [ - 'is_error' => TRUE, - 'error' => $e->getLogMessage(), - ]; - } - } - if ($adGroupMailingList->getGroupDescription() !== $params['description']) { - try { - $adGroupMailingList->updateGroupDescription($params['description']); - \Civi::log(E::LONG_NAME)->info( - "Updated group '{$adGroupMailingList->getId()}' description.'" - ); - $result['group_updated']['description'] = [ - 'is_error' => FALSE, - 'old' => $adGroupMailingList->getGroupDescription(), - 'new' => $params['description'], - ]; - } - catch (MailinglistException $e) { - \Civi::log(E::LONG_NAME)->error($e->getLogMessage()); - $result['group_updated']['description'] = [ - 'is_error' => TRUE, - 'error' => $e->getLogMessage(), - ]; - } - } - if ($adGroupMailingList->getEmailAddress() !== $params['email']) { - try { - $adGroupMailingList->updateEmailAddress($params['email']); - \Civi::log(E::LONG_NAME)->info( - "Updated group '{$adGroupMailingList->getId()}' email address from '{$adGroupMailingList->getEmailAddress()}' to '{$params['email']}'" - ); - $result['group_updated']['email'] = [ - 'is_error' => FALSE, - 'old' => $adGroupMailingList->getEmailAddress(), - 'new' => $params['email'], - ]; - } - catch (MailinglistException $e) { - \Civi::log(E::LONG_NAME)->error($e->getLogMessage()); - $result['group_updated']['email'] = [ - 'is_error' => TRUE, - 'error' => $e->getLogMessage(), - ]; - } - } - $adGroupMailingList->save(); + // If group is updated + if ( + !empty($params['sid']) && + !empty($params['email']) && + !empty($params['name']) + ) { + // Prepare result array + $result = [ + 'group' => + [ + 'sid' => $params['sid'], + 'email' => $params['email'], + 'name' => $params['name'], + 'description' => $params['description'] ?? '', + 'created' => FALSE, + 'updated' => FALSE, + ], + ]; - if ($result['group_updated'] ?? FALSE) { - $result['group_updated']['error_count'] = count(array_filter($result['group_updated'], fn($v) => $v['is_error'])); - $result['group_updated']['is_error'] = $result['group_updated']['error_count'] > 0; - $result['group_updated']['group_id'] = $adGroupMailingList->getId(); + try { + // Try to get mailing list by SID + $adGroupMailingList = ADGroupMailingList::getBySID($params['sid']); + + // If no AD group mailing list found, create new + if (!$adGroupMailingList) { + try { + $adGroupMailingList = ADGroupMailingList::createGroup( + title: $params['name'], + email: $params['email'], + description: $params['description'], + sid: $params['sid'], + ); + $result['group']['created']['group_id'] = $adGroupMailingList->getId(); + $result['group']['created']['is_error'] = 0; + } + catch (MailinglistException $e) { + \Civi::log(E::LONG_NAME)->error($e->getLogMessage()); + $result['group']['created']['is_error'] = 1; + } } + + // Sync AD group mailing list values + else { + if ($adGroupMailingList->getTitle() !== $params['name']) { + try { + $adGroupMailingList->updateGroupTitle($params['name']); + \Civi::log(E::LONG_NAME)->info( + "Updated group '{$adGroupMailingList->getId()}' title from '{$adGroupMailingList->getTitle()}' to '{$params['name']}'" + ); + $result['group']['updated']['title'] = [ + 'is_error' => 0, + 'old' => $adGroupMailingList->getTitle(), + 'new' => $params['name'], + ]; + } + catch (MailinglistException $e) { + \Civi::log(E::LONG_NAME)->error($e->getLogMessage()); + $result['group']['updated']['title'] = [ + 'is_error' => 1, + 'error' => $e->getLogMessage(), + ]; + } + } + if ($adGroupMailingList->getGroupDescription() !== ($params['description'] ?? '')) { + try { + $adGroupMailingList->updateGroupDescription($params['description']); + \Civi::log(E::LONG_NAME)->info( + "Updated group '{$adGroupMailingList->getId()}' description.'" + ); + $result['group']['updated']['description'] = [ + 'is_error' => 0, + 'old' => $adGroupMailingList->getGroupDescription(), + 'new' => $params['description'], + ]; + } + catch (MailinglistException $e) { + \Civi::log(E::LONG_NAME)->error($e->getLogMessage()); + $result['group']['updated']['description'] = [ + 'is_error' => 1, + 'error' => $e->getLogMessage(), + ]; + } + } + if ($adGroupMailingList->getEmailAddress() !== $params['email']) { + try { + $adGroupMailingList->updateEmailAddress($params['email']); + \Civi::log(E::LONG_NAME)->info( + "Updated group '{$adGroupMailingList->getId()}' email address from '{$adGroupMailingList->getEmailAddress()}' to '{$params['email']}'" + ); + $result['group']['updated']['email'] = [ + 'is_error' => 0, + 'old' => $adGroupMailingList->getEmailAddress(), + 'new' => $params['email'], + ]; + } + catch (MailinglistException $e) { + \Civi::log(E::LONG_NAME)->error($e->getLogMessage()); + $result['group']['updated']['email'] = [ + 'is_error' => 1, + 'error' => $e->getLogMessage(), + ]; + } + } + $adGroupMailingList->save(); + + if ($result['group']['updated'] ?? FALSE) { + $result['group']['updated']['error_count'] = count(array_filter($result['group']['updated'], fn($v) => $v['is_error'])); + $result['group']['updated']['is_error'] = (int) ($result['group']['updated']['error_count'] > 0); + } + $result['group']['group_id'] = $adGroupMailingList->getId(); + } + } + catch (MailinglistException $me) { + \Civi::log(E::LONG_NAME)->error($me->getLogMessage()); + return civicrm_api3_create_error($me->getLogMessage(), + [ + 'values' => $result, + 'params' => $params, + 'entity' => 'Mailinglistsync', + 'action' => 'Adgroupsync', + ]); } // Sync group mailing list members - $result['recipients_updated'] = $adGroupMailingList->syncRecipients($params['recipients']); + $result['recipients'] = $adGroupMailingList->syncRecipients($params['recipients']); // Return error response if any errors occurred - $totalErrors = (int) ($result['group_created']['is_error'] ?? 0) - + ($result['group_updated']['error_count'] ?? 0) - + ($result['recipients_updated']['error_count'] ?? 0); - $result['is_error'] = $totalErrors > 0; + $totalErrors = (int) ($result['group']['created']['is_error'] ?? 0) + + ($result['group']['updated']['error_count'] ?? 0) + + ($result['recipients']['error_count'] ?? 0); + $result['is_error'] = (int) ($totalErrors > 0); $result['error_count'] = $totalErrors; if ($totalErrors > 0) { return civicrm_api3_create_error( @@ -185,15 +243,45 @@ function civicrm_api3_mailinglistsync_Adgroupsync($params) { 'entity' => 'Mailinglistsync', 'action' => 'Adgroupsync', ]); - } - // Else return success response return civicrm_api3_create_success($result, $params, 'Mailinglistsync', 'Adgroupsync'); } - catch (MailinglistException $me) { - \Civi::log(E::LONG_NAME)->error($me->getLogMessage()); - return civicrm_api3_create_error($me->getLogMessage(), + + // If only recipients are updated + elseif ( + !empty($params['recipients']) && + empty($params['sid']) && + empty($params['email']) && + empty($params['name']) && + empty($params['description']) + ) { + $result = []; + + // Update recipients + $result['recipients'] = ADGroupMailingList::syncContacts($params['recipients']); + + // Return error response if any errors occurred + $totalErrors = $result['recipients']['error_count'] ?? 0; + $result['count'] = $result['recipients']['count'] ?? 0; + $result['is_error'] = (int) ($totalErrors > 0); + $result['error_count'] = $totalErrors; + if ($totalErrors > 0) { + return civicrm_api3_create_error( + "Failed to sync recipients. $totalErrors errors occurred.", + [ + 'values' => $result, + 'params' => $params, + 'entity' => 'Mailinglistsync', + 'action' => 'Adgroupsync', + ]); + } + // Else return success response + return civicrm_api3_create_success($result ? $result['count'] : [], $params, 'Mailinglistsync', 'Adgroupsync'); + } + else { + return civicrm_api3_create_error( + 'Missing required parameters', [ 'params' => $params, 'entity' => 'Mailinglistsync', diff --git a/api/v3/Mailinglistsync/Mlmmjsync.php b/api/v3/Mailinglistsync/Mlmmjsync.php index 808b9d7..8f08ec2 100644 --- a/api/v3/Mailinglistsync/Mlmmjsync.php +++ b/api/v3/Mailinglistsync/Mlmmjsync.php @@ -1,6 +1,8 @@ getGroupQueue(); - $eventQueue = $qh->getEventQueue(); - $emailQueue = $qh->getEmailQueue(); - - // Create runners - $groupRunner = new CRM_Queue_Runner([ - 'title' => ts('ProPeace GroupMailinglist Runner'), - 'queue' => $groupQueue, - 'errorMode' => CRM_Queue_Runner::ERROR_CONTINUE, - ]); - $eventRunner = new CRM_Queue_Runner([ - 'title' => ts('ProPeace EventMailinglist Runner'), - 'queue' => $eventQueue, - 'errorMode' => CRM_Queue_Runner::ERROR_CONTINUE, - ]); - $emailRunner = new CRM_Queue_Runner([ - 'title' => ts('ProPeace EmailMailinglist Runner'), - 'queue' => $emailQueue, - 'errorMode' => CRM_Queue_Runner::ERROR_CONTINUE, - ]); - - // Run runners - $results = []; - $continue = TRUE; - while($continue) { - $result = $groupRunner->runNext(false); - if (!$result['is_continue']) { - $continue = false; + // Check if the mailing list sync is enabled + $enabled = (bool) MailingListSettings::get('enable'); + if (!$enabled) { + return civicrm_api3_create_success( + ['message' => 'Mailing list sync is disabled'], [], 'Mailinglist', 'Mlmmjsync' + ); } - $results['runners'][] = $result; - } - $groups = $qh->getGroups(); - $events = $qh->getEvents(); - $emails = $qh->getEmails(); + $is_error = FALSE; - // TODO: Sync groups and events just once and invoke syncing + // Get queues + $qh = QueueHelper::getInstance(); + $groupQueue = $qh->getGroupQueue(); + $eventQueue = $qh->getEventQueue(); + $emailQueue = $qh->getEmailQueue(); - $mailingListsToSync = []; - foreach ($groups as $group) { - $mailingListsToSync[$group->getId()] = $group; - } - foreach ($events as $event) { - $mailingListsToSync[$event->getId()] = $event; - } - foreach ($emails as $email) { - $emailGroups = $email->getGroups(); - $emailEvents = $email->getEvents(); - foreach ($emailGroups as $group) { + // Create runners + $groupRunner = new CRM_Queue_Runner([ + 'title' => ts('ProPeace GroupMailinglist Runner'), + 'queue' => $groupQueue, + 'errorMode' => CRM_Queue_Runner::ERROR_CONTINUE, + ]); + $eventRunner = new CRM_Queue_Runner([ + 'title' => ts('ProPeace EventMailinglist Runner'), + 'queue' => $eventQueue, + 'errorMode' => CRM_Queue_Runner::ERROR_CONTINUE, + ]); + $emailRunner = new CRM_Queue_Runner([ + 'title' => ts('ProPeace EmailMailinglist Runner'), + 'queue' => $emailQueue, + 'errorMode' => CRM_Queue_Runner::ERROR_CONTINUE, + ]); + + // Run runners + $results = []; + foreach ([$groupRunner, $eventRunner, $emailRunner] as $runner) { + $continue = TRUE; + $count = 0; + $errors = []; + while ($continue) { + $count++; + + $result = $runner->runNext(FALSE); + $continue = $result['is_continue']; + + if ($result['is_error']) { + $error = $result['exception']->getMessage(); + // If the error is 'Failed to claim next task' we should stop the runner + if ($error === 'Failed to claim next task') { + $continue = FALSE; + } + else { + $errors[] = $error; + } + } + } + + $results['runners'][$runner->title][] = [ + 'task_count' => $count, + 'errors' => $errors, + ]; + + $is_error = !empty($errors) || $is_error; + } + + $groups = $qh->getGroups(); + $events = $qh->getEvents(); + $emails = $qh->getEmails(); + + // Sync groups and events just once and invoke syncing + $mailingListsToSync = []; + foreach ($groups as $group) { $mailingListsToSync[$group->getId()] = $group; } - foreach ($emailEvents as $event) { + foreach ($events as $event) { $mailingListsToSync[$event->getId()] = $event; } + foreach ($emails as $email) { + $recipient = MailingListRecipient::getContactIdEmail($email); + $emailGroups = $recipient->getMailingLists(); + $emailEvents = $recipient->getEventMailingLists(); + foreach ($emailGroups as $group) { + $mailingListsToSync[$group->getId()] = $group; + } + foreach ($emailEvents as $event) { + $mailingListsToSync[$event->getId()] = $event; + } + } + + foreach ($mailingListsToSync as $mailingList) { + $results['mailing_lists'][$mailingList->getEmailAddress()] = $mailingList->sync(); // TODO: re-add failed task to queue + } + + if ($is_error) { + return civicrm_api3_create_error('One or more errors occurred during the sync process', [ + 'params' => $params, + 'results' => $results, + 'entity' => 'Mailinglistsync', + 'action' => 'Mlmmjsync', + ]); + } + return civicrm_api3_create_success($results, [], 'Mailinglist', 'Mlmmjsync'); } - - foreach ($mailingListsToSync as $mailingList) { - $results['mailing_lists'][] = $mailingList->sync(); + catch (Exception $e) { + return civicrm_api3_create_error($e->getMessage(), [ + 'params' => $params, + 'entity' => 'Mailinglistsync', + 'action' => 'Mlmmjsync', + ]); } - - return civicrm_api3_create_success($results, [], 'Mailinglist', 'Mlmmjsync'); - - /* - throw new CRM_Core_Exception('Everyone knows that the magicword is "sesame"', 'magicword_incorrect'); - */ } diff --git a/info.xml b/info.xml index eb2d7b2..41e1e8b 100644 --- a/info.xml +++ b/info.xml @@ -19,7 +19,7 @@ 2025-03-04 1.0.0 - alpha + beta 5.80 diff --git a/mailinglistsync.civix.php b/mailinglistsync.civix.php index 8dcc2b5..040a3a3 100644 --- a/mailinglistsync.civix.php +++ b/mailinglistsync.civix.php @@ -155,7 +155,7 @@ function _mailinglistsync_civix_insert_navigation_menu(&$menu, $path, $item) { $path = explode('/', $path); $first = array_shift($path); foreach ($menu as $key => &$entry) { - if ($entry['attributes']['name'] == $first) { + if ($entry['attributes']['name'] === $first) { if (!isset($entry['child'])) { $entry['child'] = []; } diff --git a/mailinglistsync.php b/mailinglistsync.php index 605c139..97ee9df 100644 --- a/mailinglistsync.php +++ b/mailinglistsync.php @@ -9,7 +9,6 @@ use Civi\Mailinglistsync\Exceptions\MailinglistException; use CRM_Mailinglistsync_ExtensionUtil as E; use Civi\Mailinglistsync\EventMailingList; use Civi\Mailinglistsync\GroupMailingList; -use Civi\Mailinglistsync\ADGroupMailingList; use function Civi\Mailinglistsync\getLocationTypes; require_once 'Civi/Mailinglistsync/Utils.php'; @@ -49,27 +48,152 @@ function mailinglistsync_civicrm_enable(): void { */ function mailinglistsync_civicrm_validateForm($formName, &$fields, &$files, &$form, &$errors): void { // Validate custom fields in group and event forms. - if ($formName == 'CRM_Group_Form_Edit') { - $mailingList = new GroupMailingList(); + if ($formName === 'CRM_Group_Form_Edit') { + if ($_REQUEST['action'] === 'delete' || $_REQUEST['action'] === 'update') { + $mailingList = new GroupMailingList(intval($_REQUEST['id'])); + } + else { + $mailingList = new GroupMailingList(); + } } - elseif ($formName == 'CRM_Event_Form_ManageEvent_EventInfo') { - $mailingList = new EventMailingList(); + elseif ($formName === 'CRM_Event_Form_ManageEvent_EventInfo') { + if ($_REQUEST['action'] === 'delete' || $_REQUEST['action'] === 'update') { + $mailingList = new EventMailingList(intval($_REQUEST['id'])); + } + else { + $mailingList = new EventMailingList(); + } } if (!empty($mailingList)) { + // We cannot delete the corresponding mlmmj mailing list in the post hook + // so we have to deal with it here + if ($_REQUEST['action'] === 'delete' && $mailingList->isEnabled()) { + try { + $mailingList->delete(); + } + catch (MailinglistException $e) { + $errorCode = $e->getCode(); + + if ($errorCode !== MailinglistException::ERROR_CODE_DELETE_EMAIL_ADDRESS_FAILED) { + \Civi::log(E::LONG_NAME)->error($e->getMessage()); + } + CRM_Core_Session::setStatus( + E::ts('Failed to delete mlmmj mailing list: %1', [1 => $e->getMessage()]), + E::ts('Deletion failed'), + 'alert', + ); + } + } + + // Handle group and event mailing list updates which affect the mlmmj mailing list + if ($_REQUEST['action'] === 'update' && $mailingList->isEnabled()) { + $customFields = $mailingList::translateCustomFields($fields); + + // If enabled changes from TRUE to FALSE, delete the mlmmj mailing list + if ($customFields['Enable_mailing_list']['value'] === "0") { + try { + $mailingList->delete(); + } + catch (MailinglistException $e) { + $errorCode = $e->getCode(); + if ($errorCode !== MailinglistException::ERROR_CODE_DELETE_EMAIL_ADDRESS_FAILED) { + \Civi::log(E::LONG_NAME)->error($e->getMessage()); + } + CRM_Core_Session::setStatus( + E::ts('Failed to delete mlmmj mailing list: %1', [1 => $e->getMessage()]), + E::ts('Deletion failed'), + 'alert', + ); + } + } + + // If email address has changed, delete and mark group for creation + elseif ($customFields['E_mail_address']['value'] !== $mailingList->getEmailAddress()) { + try { + $mailingList->delete(); + } + catch (MailinglistException $e) { + $errorCode = $e->getCode(); + if ($errorCode !== MailinglistException::ERROR_CODE_DELETE_EMAIL_ADDRESS_FAILED) { + \Civi::log(E::LONG_NAME)->error($e->getMessage()); + } + CRM_Core_Session::setStatus( + E::ts('Failed to delete mlmmj mailing list: %1', [1 => $e->getMessage()]), + E::ts('Deletion failed'), + 'alert', + ); + } + // Queue mailinglist for synchronization with mlmmj + if ($mailingList::class === GroupMailingList::class) { + $queue = \Civi::queue('propeace-mailinglist-group-queue', [ + 'type' => 'SqlParallel', + 'is_autorun' => FALSE, + 'reset' => FALSE, + 'error' => 'drop', + ]); + $queue->createItem(new CRM_Queue_Task( + // callback + ['Civi\Mailinglistsync\QueueHelper', 'storeInstance'], + // arguments + [$mailingList->getId(), $mailingList::class], + // title + "Sync Group ID '{$mailingList->getId()}'" + )); + } + elseif ($mailingList::class === EventMailingList::class) { + $queue = \Civi::queue('propeace-mailinglist-event-queue', [ + 'type' => 'SqlParallel', + 'is_autorun' => FALSE, + 'reset' => FALSE, + 'error' => 'drop', + ]); + $queue->createItem(new CRM_Queue_Task( + // callback + ['Civi\Mailinglistsync\QueueHelper', 'storeInstance'], + // arguments + [$mailingList->getId(), $mailingList::class], + // title + "Sync Event ID '{$mailingList->getId()}'" + )); + } + } + } + + // Validate custom fields $mailingList::validateCustomFields($fields, $form, $errors); } // Check permission to alter group membership via form - if ($formName == 'CRM_Contact_Form_GroupContact' || $formName == 'CRM_Contact_Form_Task_AddToGroup') { + if ($formName === 'CRM_Contact_Form_GroupContact' || $formName === 'CRM_Contact_Form_Task_AddToGroup') { $mailingList = new GroupMailingList($fields['group_id']); - _check_group_membership_permissions($mailingList, $errors); + _mailinglistsync_check_group_membership_permissions($mailingList, $errors); } - elseif ($formName == 'CRM_Event_Form_Participant') { + elseif ($formName === 'CRM_Event_Form_Participant') { $mailingList = new EventMailingList($fields['event_id']); _mailinglistsync_check_event_membership_permissions($mailingList, $errors); } } + + +function mailinglistsync_civicrm_pre($op, $objectName, $objectId, &$params) { + if ($op === 'delete' || $op === 'edit') { + + if ($objectName === 'Group' || $objectName === 'Event') { + $mailingList = $objectName === 'Group' + ? new GroupMailingList($objectId) + : new EventMailingList($objectId); + if ($mailingList->isEnabled()) { + + // If email has changed, delete the mailing list and create a new one + if ($mailingList->getEmailAddress() !== $params['email']) { + // + } + } + } + } +} + /** * Implements hook_civicrm_post() to check on permissions to alter mailing list * groups and sync group with mlmmj. @@ -79,14 +203,14 @@ function mailinglistsync_civicrm_validateForm($formName, &$fields, &$files, &$fo * @throws \CRM_Core_Exception */ function mailinglistsync_civicrm_post(string $op, string $objectName, int $objectId, &$objectRef) { - if ($op == 'delete' || $op == 'edit' || $op == 'create') { + if ($op === 'delete' || $op === 'edit' || $op === 'create') { // Check on groups mailing list // Note: I wonder why $objectId refers to the group instead of the group contact here. - if ($objectName == 'GroupContact') { - $mailingList = new GroupMailingList($objectId); + if ($objectName === 'GroupContact') { + $mailingList = new GroupMailingList($objectRef->group_id ?? $objectId); // Check permission to alter group membership - if ($mailingList->isMailingList() && !CRM_Core_Permission::check('manage_group_mailinglists')) { + if ($mailingList->isEnabled() && !CRM_Core_Permission::check('manage_group_mailinglists')) { CRM_Core_Session::setStatus( E::ts('You do not have permission to manage memberships of mailing list groups.'), E::ts('Permission Denied'), @@ -113,7 +237,7 @@ function mailinglistsync_civicrm_post(string $op, string $objectName, int $objec } // Check on event mailing lists - elseif ($objectName == 'Participant') { + elseif ($objectName === 'Participant') { $eventId = $objectRef->event_id ?? Event::get() ->addSelect('event_id') ->addWhere('id', '=', $objectId) @@ -122,7 +246,7 @@ function mailinglistsync_civicrm_post(string $op, string $objectName, int $objec $mailingList = new GroupMailingList($eventId); // Check permission to alter event mailing list - if ($mailingList->isMailingList() && !CRM_Core_Permission::check('manage_event_mailinglists')) { + if ($mailingList->isEnabled() && !CRM_Core_Permission::check('manage_event_mailinglists')) { CRM_Core_Session::setStatus( E::ts('You do not have permission to manage event mailing lists.'), E::ts('Permission Denied'), @@ -135,18 +259,8 @@ function mailinglistsync_civicrm_post(string $op, string $objectName, int $objec } } - // Delete mlmmj mailing list - if ( - $op == 'delete' && - !empty($mailingList) && - ($mailingList->isMailingList() || $mailingList->isADGroup()) - ) { - $mailingList->deleteMailingList(); // TODO - } - // If this is an e-mail address of a location type used for mailing lists - if ($objectName == 'Email' && in_array((int) $objectRef->location_type_id, array_keys(getLocationTypes()))) { - + if ($objectName === 'Email' && in_array((int) $objectRef->location_type_id, array_keys(getLocationTypes()))) { // Ensure that only one e-mail address of this location type is set for this contact $result = Email::get() ->addSelect('contact_id', 'contact.display_name') @@ -160,7 +274,7 @@ function mailinglistsync_civicrm_post(string $op, string $objectName, int $objec if (!empty($result) && $op != 'delete') { throw new MailinglistException( - E::ts("An e-mail address of a mailing list type is already used for this contact"), + "An e-mail address of a mailing list type is already used for this contact", MailinglistException::ERROR_CODE_UPDATE_EMAIL_ADDRESS_FAILED ); } @@ -168,7 +282,11 @@ function mailinglistsync_civicrm_post(string $op, string $objectName, int $objec // Ensure that this email address is only used for one contact $results = Email::get() ->addSelect('contact.id', 'contact.display_name') - ->addJoin('Contact AS contact', 'LEFT', ['contact_id', '=', 'contact.id']) + ->addJoin('Contact AS contact', 'LEFT', [ + 'contact_id', + '=', + 'contact.id', + ]) ->addWhere('contact_id', '!=', $objectRef->contact_id) ->addWhere('email', '=', $objectRef->email) ->addWhere('location_type_id', '=', $objectRef->location_type_id) @@ -176,7 +294,6 @@ function mailinglistsync_civicrm_post(string $op, string $objectName, int $objec ->getArrayCopy(); if (!empty($results) && $op != 'delete') { - // Delete e-mail address if it is not allowed // TODO: This is a workaround. We should prevent the creation of the e-mail address in the first place. _mailinglistsync_delete_email_address((int) $objectRef->id); @@ -219,9 +336,9 @@ function mailinglistsync_civicrm_post(string $op, string $objectName, int $objec function mailinglistsync_civicrm_postCommit(string $op, string $objectName, int $objectId, &$objectRef) { // Sync groups mailing list // Note: I wonder why $objectId refers to the group instead of the group contact here. - if ($objectName == 'GroupContact') { - $mailingList = new GroupMailingList($objectId); - if ($mailingList->isMailingList() || $mailingList->isADGroup()) { + if ($objectName === 'GroupContact') { + $mailingList = new GroupMailingList($objectRef->group_id ?? $objectId); + if ($mailingList->isEnabled()) { // Queue group for synchronization with mlmmj $queue = \Civi::queue('propeace-mailinglist-group-queue', [ 'type' => 'SqlParallel', @@ -235,20 +352,41 @@ function mailinglistsync_civicrm_postCommit(string $op, string $objectName, int // arguments [$mailingList->getId(), $mailingList::class], // title - "Sync Group ID '$objectId'" + "Sync Group ID '{$mailingList->getId()}'" + )); + } + } + + elseif ($objectName === 'Group') { + $mailingList = new GroupMailingList($objectId); + if ($mailingList->isEnabled()) { + // Queue group for synchronization with mlmmj + $queue = \Civi::queue('propeace-mailinglist-group-queue', [ + 'type' => 'SqlParallel', + 'is_autorun' => FALSE, + 'reset' => FALSE, + 'error' => 'drop', + ]); + $queue->createItem(new CRM_Queue_Task( + // callback + ['Civi\Mailinglistsync\QueueHelper', 'storeInstance'], + // arguments + [$mailingList->getId(), $mailingList::class], + // title + "Sync Group ID '{$mailingList->getId()}'" )); } } // Sync event mailing lists - elseif ($objectName == 'Participant') { + elseif ($objectName === 'Participant') { $eventId = $objectRef->event_id ?? Participant::get() ->addSelect('event_id') ->addWhere('id', '=', $objectId) ->execute() ->first()['event_id']; $mailingList = new EventMailingList($eventId); - if ($mailingList->isMailingList()) { + if ($mailingList->isEnabled()) { // Queue event for synchronization with mlmmj $queue = \Civi::queue('propeace-mailinglist-event-queue', [ 'type' => 'SqlParallel', @@ -262,43 +400,86 @@ function mailinglistsync_civicrm_postCommit(string $op, string $objectName, int // arguments [$mailingList->getId(), $mailingList::class], // title - "Sync Event ID '$objectId'" + "Sync Event ID '{$mailingList->getId()}'" + )); + } + } + + elseif ($objectName === 'Event') { + $mailingList = new EventMailingList($objectId); + if ($mailingList->isEnabled()) { + // Queue event for synchronization with mlmmj + $queue = \Civi::queue('propeace-mailinglist-event-queue', [ + 'type' => 'SqlParallel', + 'is_autorun' => FALSE, + 'reset' => FALSE, + 'error' => 'drop', + ]); + $queue->createItem(new CRM_Queue_Task( + // callback + ['Civi\Mailinglistsync\QueueHelper', 'storeInstance'], + // arguments + [$mailingList->getId(), $mailingList::class], + // title + "Sync Event ID '{$mailingList->getId()}'" + )); + } + } + + if ($objectName === 'GroupContact') { + $mailingList = new GroupMailingList($objectRef->group_id ?? $objectId); + if ($mailingList->isEnabled()) { + // Queue group for synchronization with mlmmj + $queue = \Civi::queue('propeace-mailinglist-group-queue', [ + 'type' => 'SqlParallel', + 'is_autorun' => FALSE, + 'reset' => FALSE, + 'error' => 'drop', + ]); + $queue->createItem(new CRM_Queue_Task( + // callback + ['Civi\Mailinglistsync\QueueHelper', 'storeInstance'], + // arguments + [$mailingList->getId(), $mailingList::class], + // title + "Sync Group ID '{$mailingList->getId()}'" )); } } // Sync e-mail addresses - elseif ($objectName == 'Email' && in_array((int) $objectRef->location_type_id, array_keys(getLocationTypes()))) { - - // Only sync e-mail addresses of mailing list types - $locationType = \Civi\Api4\LocationType::get() - ->addSelect('name') - ->addWhere('id', '=', $objectRef->location_type_id) - ->execute() - ->first()['name']; - if (!in_array($locationType, [ - GroupMailingList::LOCATION_TYPE, - ADGroupMailingList::LOCATION_TYPE, - EventMailingList::LOCATION_TYPE, - ])) { - return; + elseif ($objectName === 'Email') { + // If an email address update comes from an api call, the + // objectRef->location_type_id is not set. So we have to get it from the + // database. + if (empty($objectRef->location_type_id)) { + $locationTypeId = \Civi\Api4\Email::get() + ->addSelect('location_type_id') + ->addWhere('id', '=', $objectRef->id) + ->addWhere('location_type_id', 'IN', array_keys(getLocationTypes())) + ->execute() + ->first()['location_type_id']; + } + if ( + !empty($locationTypeId) || + in_array((int) $objectRef->location_type_id, array_keys(getLocationTypes())) + ) { + // Queue email address for synchronization with mlmmj + $queue = \Civi::queue('propeace-mailinglist-email-queue', [ + 'type' => 'SqlParallel', + 'is_autorun' => FALSE, + 'reset' => FALSE, + 'error' => 'drop', + ]); + $queue->createItem(new CRM_Queue_Task( + // callback + ['Civi\Mailinglistsync\QueueHelper', 'storeEmail'], + // arguments + [$objectRef->email], + // title + "Sync E-Mail '$objectRef->email'" + )); } - - // Queue email address for synchronization with mlmmj - $queue = \Civi::queue('propeace-mailinglist-email-queue', [ - 'type' => 'SqlParallel', - 'is_autorun' => FALSE, - 'reset' => FALSE, - 'error' => 'drop', - ]); - $queue->createItem(new CRM_Queue_Task( - // callback - ['Civi\Mailinglistsync\QueueHelper', 'addToEmails'], - // arguments - [$objectRef->email], - // title - "Sync E-Mail '$objectRef->email'" - )); } } @@ -330,6 +511,23 @@ function mailinglistsync_civicrm_permission(&$permissions) { ]; } +/** + * Check permissions to alter group membership. + * + * @param \Civi\Mailinglistsync\GroupMailingList $mailingList + * @param $errors + * + * @return void + */ +function _mailinglistsync_check_group_membership_permissions(GroupMailingList $mailingList, &$errors): void { + if ($mailingList->isEnabled() && !CRM_Core_Permission::check('manage_group_mailinglists')) { + $errors['group_id'] = E::ts('You do not have permission to manage group membership.'); + } + if ($mailingList->isADGroup() && !CRM_Core_Permission::check('manage_ad_mailinglists')) { + $errors['group_id'] = E::ts('You do not have permission to manage AD group membership.'); + } +} + /** * Check permissions to alter event membership. * @@ -339,7 +537,7 @@ function mailinglistsync_civicrm_permission(&$permissions) { * @return void */ function _mailinglistsync_check_event_membership_permissions(EventMailingList $mailingList, &$errors): void { - if ($mailingList->isMailingList() && !CRM_Core_Permission::check('manage_event_mailinglists')) { + if ($mailingList->isEnabled() && !CRM_Core_Permission::check('manage_event_mailinglists')) { $errors['event_id'] = E::ts('You do not have permission to manage event membership.'); } } @@ -361,7 +559,7 @@ function _mailinglistsync_delete_email_address(int $emailId): void { } catch (\Exception $e) { throw new MailinglistException( - E::ts('Failed to delete e-mail address'), + "Failed to delete e-mail address: {$e}", MailinglistException::ERROR_CODE_DELETE_EMAIL_ADDRESS_FAILED ); } diff --git a/managed/CustomGroup_Active_Directory.mgd.php b/managed/CustomGroup_Active_Directory.mgd.php index 90662b4..c59d5af 100644 --- a/managed/CustomGroup_Active_Directory.mgd.php +++ b/managed/CustomGroup_Active_Directory.mgd.php @@ -39,10 +39,11 @@ return [ 'is_searchable' => TRUE, 'help_post' => E::ts('The SID of this contact in the Active Directory.'), 'is_view' => TRUE, - 'text_length' => 36, + 'text_length' => 50, 'note_columns' => 60, 'note_rows' => 4, 'column_name' => 'sid_42', + 'is_active' => TRUE, ], 'match' => [ 'name', diff --git a/managed/CustomGroup_Event_Mailing_List.mgd.php b/managed/CustomGroup_Event_Mailing_List.mgd.php index 21b80c5..43bed2f 100644 --- a/managed/CustomGroup_Event_Mailing_List.mgd.php +++ b/managed/CustomGroup_Event_Mailing_List.mgd.php @@ -44,6 +44,7 @@ return [ 'note_columns' => 60, 'note_rows' => 4, 'column_name' => 'enable_mailing_list_30', + 'is_active' => TRUE, ], 'match' => [ 'name', @@ -68,6 +69,7 @@ return [ 'note_columns' => 60, 'note_rows' => 4, 'column_name' => 'e_mail_address_10', + 'is_active' => TRUE, ], 'match' => [ 'name', @@ -92,6 +94,7 @@ return [ 'note_columns' => 60, 'note_rows' => 4, 'column_name' => 'subject_prefix_11', + 'is_active' => TRUE, ], 'match' => [ 'name', @@ -118,6 +121,7 @@ return [ 'note_columns' => 60, 'note_rows' => 4, 'column_name' => 'can_be_reached_externally_12', + 'is_active' => TRUE, ], 'match' => [ 'name', diff --git a/managed/CustomGroup_Group_Mailing_List.mgd.php b/managed/CustomGroup_Group_Mailing_List.mgd.php index b46be0d..df6b098 100644 --- a/managed/CustomGroup_Group_Mailing_List.mgd.php +++ b/managed/CustomGroup_Group_Mailing_List.mgd.php @@ -44,6 +44,7 @@ return [ 'note_columns' => 60, 'note_rows' => 4, 'column_name' => 'enable_mailing_list_31', + 'is_active' => TRUE, ], 'match' => [ 'name', @@ -68,6 +69,7 @@ return [ 'note_columns' => 60, 'note_rows' => 4, 'column_name' => 'e_mail_address_10', + 'is_active' => TRUE, ], 'match' => [ 'name', @@ -89,10 +91,11 @@ return [ 'html_type' => 'Text', 'help_post' => E::ts('The SID of the group in the Active Directory.'), 'is_view' => TRUE, - 'text_length' => 36, + 'text_length' => 50, 'note_columns' => 60, 'note_rows' => 4, 'column_name' => 'active_directory_SID_11', + 'is_active' => TRUE, ], 'match' => [ 'name', @@ -119,6 +122,7 @@ return [ 'note_columns' => 60, 'note_rows' => 4, 'column_name' => 'can_be_reached_externally_12', + 'is_active' => TRUE, ], 'match' => [ 'name', @@ -143,6 +147,7 @@ return [ 'note_columns' => 60, 'note_rows' => 4, 'column_name' => 'subject_prefix_13', + 'is_active' => TRUE, ], 'match' => [ 'name', diff --git a/resources/activity_types.php b/resources/activity_types.php new file mode 100644 index 0000000..984361b --- /dev/null +++ b/resources/activity_types.php @@ -0,0 +1,11 @@ + 'Mailing_List_Synchronization_Activity', + 'label' => E::ts('Mailing List Synchronization Activity'), + 'description' => E::ts('Indicates a contact has been synchronized with a mailing list'), + ], +]; diff --git a/templates/CRM/Mailinglistsync/Form/MailingListSettings.tpl b/templates/CRM/Mailinglistsync/Form/MailingListSettings.tpl index 06ccaa3..f171d71 100644 --- a/templates/CRM/Mailinglistsync/Form/MailingListSettings.tpl +++ b/templates/CRM/Mailinglistsync/Form/MailingListSettings.tpl @@ -1,66 +1,99 @@ -
-
{$form[$EXTENSION_SHORT_NAME|cat:'_enable'].label}
-
{$form[$EXTENSION_SHORT_NAME|cat:'_enable'].html}
-
-
-
-
{$form[$EXTENSION_SHORT_NAME|cat:'_logging'].label}
-
{$form[$EXTENSION_SHORT_NAME|cat:'_logging'].html}
-
+{* HEADER *} + +
+
+
{$form.mailinglistsync_enable.label}
+
{$form.mailinglistsync_enable.html}
+
+
+
+
{$form.mailinglistsync_logging.label}
+
{$form.mailinglistsync_logging.html}
+
+
+
+
{$form.mailinglistsync_domain.label}
+
{$form.mailinglistsync_domain.html}
+
+
-

{ts}AD Mailing List Settings{/ts}

+
+

{ts}AD Mailing List Settings{/ts}

-
-
{$form[$EXTENSION_SHORT_NAME|cat:'_ad_contact_tags'].label}
-
{$form[$EXTENSION_SHORT_NAME|cat:'_ad_contact_tags'].html}
-
+
+
{$form.mailinglistsync_ad_contact_tags.label}
+
{$form.mailinglistsync_ad_contact_tags.html}
+
+
-

{ts}Event Mailing List Settings{/ts}

+
+

{ts}Event Mailing List Settings{/ts}

-
-
{$form[$EXTENSION_SHORT_NAME|cat:'_participant_status'].label}
-
{$form[$EXTENSION_SHORT_NAME|cat:'_participant_status'].html}
-
+
+
{$form.mailinglistsync_participant_status.label}
+
{$form.mailinglistsync_participant_status.html}
+
+
-

{ts}mlmmj Settings{/ts}

+
+

{ts}mlmmj Settings{/ts}

-
-
{$form[$EXTENSION_SHORT_NAME|cat:'_mlmmj_host'].label}
-
{$form[$EXTENSION_SHORT_NAME|cat:'_mlmmj_host'].html}
-
-
-
-
{$form[$EXTENSION_SHORT_NAME|cat:'_mlmmj_token'].label}
-
{$form[$EXTENSION_SHORT_NAME|cat:'_mlmmj_token'].html}
-
-
-
-
{$form[$EXTENSION_SHORT_NAME|cat:'_mlmmj_port'].label}
-
{$form[$EXTENSION_SHORT_NAME|cat:'_mlmmj_port'].html}
-
+
+
{$form.mailinglistsync_mlmmj_host.label}
+
{$form.mailinglistsync_mlmmj_host.html}
+
+
+
+
{$form.mailinglistsync_mlmmj_token.label}
+
{$form.mailinglistsync_mlmmj_token.html}
+
+
+
+
{$form.mailinglistsync_mlmmj_port.label}
+
{$form.mailinglistsync_mlmmj_port.html}
+
+
-

{ts}Dovecot Settings{/ts}

+
+

{ts}Dovecot Settings{/ts}

-
-
{$form[$EXTENSION_SHORT_NAME|cat:'_dovecot_host'].label}
-
{$form[$EXTENSION_SHORT_NAME|cat:'_dovecot_host'].html}
-
+
+
{$form.mailinglistsync_dovecot_host.label}
+
{$form.mailinglistsync_dovecot_host.html}
+
+
+
+
{$form.mailinglistsync_dovecot_token.label}
+
{$form.mailinglistsync_dovecot_token.html}
+
+
+
+
{$form.mailinglistsync_dovecot_port.label}
+
{$form.mailinglistsync_dovecot_port.html}
+
+
-
-
{$form[$EXTENSION_SHORT_NAME|cat:'_dovecot_token'].label}
-
{$form[$EXTENSION_SHORT_NAME|cat:'_dovecot_token'].html}
-
-
-
-
{$form[$EXTENSION_SHORT_NAME|cat:'_dovecot_port'].label}
-
{$form[$EXTENSION_SHORT_NAME|cat:'_dovecot_port'].html}
-
-
-
{include file="CRM/common/formButtons.tpl" location="bottom"}