Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f3aedd1b authored by Ronak Patel's avatar Ronak Patel
Browse files

Fix users with no email attribute set

parent ae4bb666
Loading
Loading
Loading
Loading
+28 −0
Original line number Diff line number Diff line
@@ -33,6 +33,34 @@
- This plugin calls the postDelete.php script in the /e/ docker-welcome container 
- The e_welcome_secret is loaded in nextcloud's config file during ecloud-selfhosting installation. 

## Fix Missing Email Addresses

- This app provides a command to fix missing email addresses by querying LDAP and setting them in NextCloud

### Usage

```bash
# Fix all users without email addresses (dry run first)
occ ecloud-accounts:fix-missing-emails --dry-run

# Fix all users without email addresses
occ ecloud-accounts:fix-missing-emails

# Fix specific users (multiple --users arguments)
occ ecloud-accounts:fix-missing-emails --users=user1 --users=user2 --users=user3

# Fix specific users with dry run
occ ecloud-accounts:fix-missing-emails --users=user1 --users=user2 --dry-run
```

### What it does

1. Queries the database to find users without email addresses set in NextCloud
2. For each user, checks if they are on the LDAP backend
3. Queries LDAP to get the `mailAddress` attribute
4. Sets only the email address in NextCloud (quota remains unchanged)
5. Provides detailed output of the process and any errors encountered

## Support

Please open issues here : https://gitlab.e.foundation/e/backlog/issues
+2 −1
Original line number Diff line number Diff line
@@ -10,7 +10,7 @@
    <description><![CDATA[in /e/OS cloud, nextcloud accounts are linked to mail accounts. This app ensures both are coordinated: it sets the e-mail address, quota and storage of the user upon creation.
    It also completes the account deletion by cleaning other parts of the /e/OS cloud setup to ensure no more data is retained when a user requests an account deletion.
    This app uses the UserDeletedEvent to invoke scripts in the docker-welcome container of /e/OS cloud setup]]></description>
    <version>10.0.5</version>
    <version>10.0.6</version>
    <licence>agpl</licence>
    <author mail="dev@murena.com" homepage="https://murena.com/">Murena SAS</author>
    <namespace>EcloudAccounts</namespace>
@@ -28,5 +28,6 @@
        <command>OCA\EcloudAccounts\Command\Migrate2FASecrets</command>
        <command>OCA\EcloudAccounts\Command\MigrateWebmailAddressbooks</command>
        <command>OCA\EcloudAccounts\Command\MapActiveAttributetoLDAP</command>
        <command>OCA\EcloudAccounts\Command\FixMissingEmails</command>
    </commands>
</info>
+173 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace OCA\EcloudAccounts\Command;

use Exception;
use OCA\EcloudAccounts\AppInfo\Application;
use OCA\EcloudAccounts\Service\LDAPConnectionService;
use OCA\EcloudAccounts\Service\UserService;
use OCP\IDBConnection;
use OCP\IUserManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class FixMissingEmails extends Command {
	private LDAPConnectionService $ldapConnectionService;
	private UserService $userService;
	private IDBConnection $dbConnection;
	private IUserManager $userManager;

	public function __construct(
		LDAPConnectionService $ldapConnectionService,
		UserService $userService,
		IDBConnection $dbConnection,
		IUserManager $userManager
	) {
		parent::__construct();
		$this->ldapConnectionService = $ldapConnectionService;
		$this->userService = $userService;
		$this->dbConnection = $dbConnection;
		$this->userManager = $userManager;
	}

	protected function configure(): void {
		$this
			->setName(Application::APP_ID . ':fix-missing-emails')
			->setDescription('Fixes missing email addresses by querying LDAP and setting them in NextCloud')
			->addOption(
				'users',
				null,
				InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
				'Users to fix (can be specified multiple times, e.g., --users=user1 --users=user2)',
				[]
			)
			->addOption(
				'dry-run',
				null,
				InputOption::VALUE_NONE,
				'Show what would be done without making changes'
			);
	}

	protected function execute(InputInterface $input, OutputInterface $output): int {
		try {
			$isDryRun = $input->getOption('dry-run');
			$usernames = $input->getOption('users');

			if ($isDryRun) {
				$output->writeln('<info>DRY RUN MODE - No changes will be made</info>');
			}

			// Get users without email
			$usersWithoutEmail = $this->getUsersWithoutEmail($usernames);
			
			if (empty($usersWithoutEmail)) {
				$output->writeln('<info>No users found without email addresses.</info>');
				return 0;
			}

			$output->writeln(sprintf('<info>Found %d users without email addresses.</info>', count($usersWithoutEmail)));

			$successCount = 0;
			$errorCount = 0;

			foreach ($usersWithoutEmail as $username) {
				try {
					$this->fixUserEmail($username, $isDryRun, $output);
					$successCount++;
				} catch (Exception $e) {
					$output->writeln(sprintf('<error>Error fixing email for user %s: %s</error>', $username, $e->getMessage()));
					$errorCount++;
				}
			}

			$output->writeln(sprintf('<info>Processed %d users successfully, %d errors.</info>', $successCount, $errorCount));
			return 0;
		} catch (Exception $e) {
			$output->writeln(sprintf('<error>Error: %s</error>', $e->getMessage()));
			return 1;
		}
	}

	/**
	 * Get users without email addresses
	 *
	 * @param array $usernames Array of usernames to check (if empty, all users without email will be processed)
	 * @return array Array of usernames without email
	 */
	private function getUsersWithoutEmail(array $usernames = []): array {
		$query = $this->dbConnection->getQueryBuilder();
		$query->select('a.uid')
			->from('accounts', 'a')
			->leftJoin('a', 'preferences', 'p', $query->expr()->andX(
				$query->expr()->eq('a.uid', 'p.userid'),
				$query->expr()->eq('p.appid', $query->createNamedParameter('settings')),
				$query->expr()->eq('p.configkey', $query->createNamedParameter('email'))
			))
			->where($query->expr()->isNull('p.userid'));

		if (!empty($usernames)) {
			$placeholders = [];
			foreach ($usernames as $username) {
				$placeholders[] = $query->createNamedParameter($username);
			}
			$query->andWhere($query->expr()->in('a.uid', $placeholders));
		}

		$result = null;
		$users = [];
		try {
			$result = $query->execute();
			while ($row = $result->fetch()) {
				$users[] = $row['uid'];
			}
		} finally {
			if ($result !== null) {
				$result->closeCursor();
			}
		}
		return $users;
	}

	/**
	 * Fix email address for a specific user
	 *
	 * @param string $username The username to fix
	 * @param bool $isDryRun Whether this is a dry run
	 * @param OutputInterface $output The output interface for writing messages
	 * @return void
	 */
	private function fixUserEmail(string $username, bool $isDryRun, OutputInterface $output): void {
		$output->writeln(sprintf('Processing user: %s', $username));

		// Check if user exists and is on LDAP backend
		$user = $this->userManager->get($username);
		if (!$user) {
			throw new Exception("User not found in NextCloud");
		}

		if (!$this->ldapConnectionService->isUserOnLDAPBackend($user)) {
			throw new Exception("User is not on LDAP backend");
		}

		// Get mailAddress directly from LDAP
		$mailAddress = $this->ldapConnectionService->getUserMailAddress($username);

		if (empty($mailAddress)) {
			throw new Exception("No mailAddress found in LDAP for user");
		}

		$output->writeln(sprintf('  Found email in LDAP: %s', $mailAddress));

		if (!$isDryRun) {
			$this->userService->setUserEmail($username, $mailAddress);
			$output->writeln(sprintf('  <info>Email set successfully for user: %s</info>', $username));
			return;
		}
		$output->writeln(sprintf('  <comment>Would set email to: %s</comment>', $mailAddress));
	}
}
+39 −0
Original line number Diff line number Diff line
@@ -122,4 +122,43 @@ class LDAPConnectionService {

		$this->closeLDAPConnection($conn);
	}

	/**
	 * Get mailAddress for a user directly from LDAP
	 *
	 * @param string $username The username to search for
	 * @return string|null The mailAddress or null if not found
	 * @throws Exception If LDAP is not enabled or user not found
	 */
	public function getUserMailAddress(string $username): ?string {
		if (!$this->isLDAPEnabled()) {
			throw new Exception('LDAP backend is not enabled');
		}

		$conn = $this->getLDAPConnection();
		$userDn = $this->username2dn($username);

		if ($userDn === false) {
			throw new Exception('Could not find DN for username: ' . $username);
		}

		$result = ldap_read($conn, $userDn, '(objectClass=*)', ['mailAddress']);
		
		if (!$result) {
			$this->closeLDAPConnection($conn);
			throw new Exception('Could not read user entry from LDAP for username: ' . $username);
		}

		$entries = ldap_get_entries($conn, $result);
		$this->closeLDAPConnection($conn);

		if ($entries['count'] === 0) {
			throw new Exception('User not found in LDAP: ' . $username);
		}

		$entry = $entries[0];
		$mailAddress = $entry['mailaddress'][0] ?? null;

		return $mailAddress;
	}
}
+16 −0
Original line number Diff line number Diff line
@@ -386,6 +386,22 @@ class UserService {
		$user->setQuota($quota);
	}

	/**
	 * Set only the email address for a user without modifying quota.
	 *
	 * @param string $uid The unique identifier of the user.
	 * @param string $mailAddress The email address to set for the user.
	 *
	 * @return void
	 */
	public function setUserEmail(string $uid, string $mailAddress): void {
		$user = $this->getUser($uid);
		if (is_null($user)) {
			throw new Exception("User with username '$uid' not found.");
		}
		$user->setEMailAddress($mailAddress);
	}

	public function isUsernameTaken(string $username) : bool {
		$commonServicesURL = $this->apiConfig['commonServicesURL'];
		$commonApiVersion = $this->apiConfig['commonApiVersion'];