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 @@