<?php

use LAM\TYPES\TypeManager;

/*

  This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/)
  Copyright (C) 2014 - 2026  Roland Gruber

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

/**
 * Provides NIS mail alias management.
 *
 * @package modules
 * @author Roland Gruber
 */

/**
 * Provides NIS mail alias management.
 *
 * @package modules
 */
class nisMailAliasUser extends baseModule {

	/** @var array<int, array<string, string[]|string>>|null $cachedAliasList alias cache */
	private ?array $cachedAliasList = null;

	/** @var array<string, string>[] $recipientsToDelete recipient entries to delete (list of arrays: dn => attributes) */
	private array $recipientsToDelete = [];
	/** @var string[] $aliasesToDelete complete alias entries to delete */
	private array $aliasesToDelete = [];
	/** @var array<string, array<string, string[]|string>> new alias entries (list of arrays: dn => attributes) */
	private array $aliasesToAdd = [];
	/** @var array<string, string[]> $recipientsToAdd alias entries to extend with new recipients (list of arrays: dn => recipients) */
	private array $recipientsToAdd = [];

	/**
	 * {@inheritDoc}
	 */
	public function can_manage(): bool {
		return $this->get_scope() === 'user';
	}

	/**
	 * {@inheritDoc}
	 */
	public function get_metaData(): array {
		$return = [];
		// icon
		$return['icon'] = 'mail.svg';
		// alias name
		$return["alias"] = _("Mail aliases");
		// module dependencies
		$return['dependencies'] = ['depends' => [['inetOrgPerson', 'posixAccount']], 'conflicts' => []];
		// help Entries
		$return['help'] = [
			'aliasUser' => [
				"Headline" => _("Alias names with user name"),
				"Text" => _('Sets the alias names linked to the current user name.')
			],
			'aliasMail' => [
				"Headline" => _("Alias names with email address"),
				"Text" => _('Sets the alias names linked to the user\'s email address.')
			],
			'suffix' => [
				"Headline" => _("Suffix"),
				"Text" => _("Location where new alias is stored.")
			],
			'newAlias' => [
				"Headline" => _("Alias name"), 'attr' => 'cn',
				"Text" => _("Name of new alias entry.")
			],
			'aliasNames' => [
				"Headline" => _("Alias names"), 'attr' => 'cn',
				"Text" => _("Select one or more alias entries from the list to add the recipient.")
			],
			'hiddenOptions' => [
				"Headline" => _("Hidden options"),
				"Text" => _("The selected options will not be managed inside LAM. You can use this to reduce the number of displayed input fields.")
			],
		];
		// available PDF fields
		$return['PDF_fields'] = [
			'alias' => _('Alias names'),
		];
		return $return;
	}

	/**
	 * {@inheritDoc}
	 */
	protected function load_Messages(): void {
		$this->messages['alias'][0] = ['ERROR', _('Alias name is invalid.')];
		$this->messages['alias'][1] = ['ERROR', _('Alias name already exists.')];
	}

	/**
	 * Returns the HTML meta data for the main account page.
	 *
	 * @return htmlElement HTML meta data
	 */
	function display_html_attributes() {
		$return = new htmlResponsiveRow();
		if (!$this->isMailAliasTypeActive()) {
			$return->add(new htmlStatusMessage('ERROR', _('Please activate the mail aliases type for this server profile.')));
			return $return;
		}
		$aliases = $this->getMailAliasList();
		$count = count($aliases);
		$userName = $this->getUserName();
		$mails = $this->getMailAddresses();
		if (!$this->isBooleanConfigOptionSet('nisMailAliasUser_hideUserAliases') && ($userName != null)) {
			$return->add(new htmlSubTitle(_('Aliases for user name')));
			for ($i = 0; $i < $count; $i++) {
				if (empty($aliases[$i]['rfc822mailmember'])) {
					continue;
				}
				$dn = $aliases[$i]['dn'];
				$members = $aliases[$i]['rfc822mailmember'];
				if (in_array($userName, $members)
					&& (!isset($this->recipientsToDelete[$dn]) || !in_array($userName, $this->recipientsToDelete[$dn]))
					&& !in_array($dn, $this->aliasesToDelete)) {
					$return->addLabel(new htmlOutputText($aliases[$i]['cn'][0]));
					$buttonGroup = new htmlGroup();
					$remButton = new htmlButton('rem_' . $i, 'del.svg', true);
					$remButton->setTitle(_('Remove user from alias entry.'));
					$buttonGroup->addElement($remButton);
					$delButton = new htmlButton('del_' . $i, 'delete.svg', true);
					$delButton->setTitle(sprintf(_('Delete whole alias entry which includes %s recipients.'), count($members)));
					$buttonGroup->addElement($delButton);
					$return->addField($buttonGroup);
				}
			}
			$return->addVerticalSpacer('1rem');
			$addButton = new htmlAccountPageButton(static::class, 'add', 'user', _('Add'));
			$return->add($addButton, 12, 12, 12, 'text-center');
		}
		if (!$this->isBooleanConfigOptionSet('nisMailAliasUser_hideMailAliases') && !empty($mails)) {
			$return->add(new htmlSubTitle(_('Aliases for email')));
			for ($m = 0; $m < count($mails); $m++) {
				if (count($mails) > 1) {
					$label = new htmlOutputText($mails[$m]);
					$return->add($label);
				}
				for ($i = 0; $i < $count; $i++) {
					if (empty($aliases[$i]['rfc822mailmember'])) {
						continue;
					}
					$dn = $aliases[$i]['dn'];
					$members = $aliases[$i]['rfc822mailmember'];
					if (in_array($mails[$m], $members)
						&& (!isset($this->recipientsToDelete[$dn]) || !in_array($mails[$m], $this->recipientsToDelete[$dn]))
						&& !in_array($dn, $this->aliasesToDelete)) {
						$return->addLabel(new htmlOutputText($aliases[$i]['cn'][0]));
						$buttonGroup = new htmlGroup();
						$remButton = new htmlButton('remMail_' . $i . '_' . $m, 'del.svg', true);
						$remButton->setTitle(_('Remove user from alias entry.'));
						$buttonGroup->addElement($remButton);
						$delButton = new htmlButton('delMail_' . $i . '_' . $m, 'delete.svg', true);
						$delButton->setTitle(sprintf(_('Delete whole alias entry which includes %s recipients.'), count($members)));
						$buttonGroup->addElement($delButton);
						$return->addField($buttonGroup);
					}
				}
				$return->addVerticalSpacer('1rem');
				$addButton = new htmlAccountPageButton(static::class, 'add', 'mail' . $m, _('Add'));
				$return->add($addButton, 12, 12, 12, 'text-center');
				if ((count($mails) > 1) && ($m < (count($mails) - 1))) {
					$return->addVerticalSpacer('2rem');
				}
			}
		}
		return $return;
	}

	/**
	 * {@inheritDoc}
	 */
	public function process_attributes(): array {
		$errors = [];
		if (!$this->isMailAliasTypeActive()) {
			return $errors;
		}
		$mails = $this->getMailAddresses();
		foreach (array_keys($_POST) as $key) {
			if (str_starts_with($key, 'rem_')) {
				$index = substr($key, strlen('rem_'));
				$dn = $this->cachedAliasList[$index]['dn'];
				$this->removeRecipient($this->getUserName(), $dn);
			}
			elseif (str_starts_with($key, 'del_')) {
				$index = substr($key, strlen('del_'));
				$dn = $this->cachedAliasList[$index]['dn'];
				$this->deleteAlias($dn);
			}
			elseif (str_starts_with($key, 'remMail_')) {
				$parts = substr($key, strlen('remMail_'));
				$parts = explode('_', $parts);
				$this->removeRecipient($mails[$parts[1]], $this->cachedAliasList[$parts[0]]['dn']);
			}
			elseif (str_starts_with($key, 'delMail_')) {
				$parts = substr($key, strlen('remMail_'));
				$parts = explode('_', $parts);
				$this->deleteAlias($this->cachedAliasList[$parts[0]]['dn']);
			}
		}
		return $errors;
	}

	/**
	 * Removes a recipient from the given DN.
	 *
	 * @param string $recipient recipient as user name or email
	 * @param string $dn alias DN
	 */
	private function removeRecipient($recipient, $dn): void {
		if (!isset($this->aliasesToAdd[$dn])) {
			// no new entry update existing
			if (isset($this->recipientsToAdd[$dn]) && in_array($recipient, $this->recipientsToAdd[$dn])) {
				// undo adding of recipient
				if (count($this->recipientsToAdd[$dn]) === 1) {
					// only one entry as marked for adding, remove whole entry
					unset($this->recipientsToAdd[$dn]);
				}
				else {
					$this->recipientsToAdd[$dn] = array_values(array_delete([$recipient], $this->recipientsToAdd[$dn]));
				}
			}
			else {
				// mark for removal
				$this->recipientsToDelete[$dn][] = $recipient;
			}
			foreach ($this->cachedAliasList as $index => $attrs) {
				if ($dn == $attrs['dn']) {
					$this->cachedAliasList[$index]['rfc822mailmember'] = array_values(array_delete([$recipient], $this->cachedAliasList[$index]['rfc822mailmember']));
				}
			}
		}
		elseif (count($this->aliasesToAdd[$dn]['rfc822mailmember']) === 1) {
			// new entry
			// single recipient in new entry, do not create new entry at all
			unset($this->aliasesToAdd[$dn]);
			foreach ($this->cachedAliasList as $index => $attrs) {
				if ($dn == $attrs['dn']) {
					unset($this->cachedAliasList[$index]);
					$this->cachedAliasList = array_values($this->cachedAliasList);
				}
			}
		}
		else {
			$this->aliasesToAdd[$dn]['rfc822mailmember'] = array_values(array_delete([$recipient], $this->aliasesToAdd[$dn]['rfc822mailmember']));
			foreach ($this->cachedAliasList as &$attrs) {
				if ($dn == $attrs['dn']) {
					$attrs['rfc822mailmember'] = array_values(array_delete([$recipient], $attrs['rfc822mailmember']));
				}
			}
		}
	}

	/**
	 * Removes an alias with the given DN.
	 *
	 * @param string $dn alias DN
	 */
	private function deleteAlias($dn): void {
		if (!isset($this->aliasesToAdd[$dn])) {
			// no new entry, delete existing entry
			$this->aliasesToDelete[] = $dn;
		}
		else {
			unset($this->aliasesToAdd[$dn]);
			foreach ($this->cachedAliasList as $index => $attrs) {
				if ($dn == $attrs['dn']) {
					unset($this->cachedAliasList[$index]);
					$this->cachedAliasList = array_values($this->cachedAliasList);
				}
			}
		}
		if (isset($this->recipientsToAdd[$dn])) {
			unset($this->recipientsToAdd[$dn]);
		}
		if (isset($this->recipientsToDelete[$dn])) {
			unset($this->recipientsToDelete[$dn]);
		}
	}

	/**
	 * Returns the HTML meta data for the add page.
	 *
	 * @return htmlElement HTML meta data
	 */
	public function display_html_add(): htmlElement {
		$return = new htmlResponsiveRow();
		$aliases = $this->getMailAliasList();
		$userName = $this->getUserName();
		$mails = $this->getMailAddresses();
		$recipient = null;
		// get recipient value to add
		if (isset($_POST['recipient'])) {
			$recipient = $_POST['recipient'];
		}
		elseif (isset($_POST['form_subpage_' . static::class . '_add_user'])) {
			$recipient = $userName;
		}
		else {
			for ($m = 0; $m < count($mails); $m++) {
				if (isset($_POST['form_subpage_' . static::class . '_add_mail' . $m])) {
					$recipient = $mails[$m];
					break;
				}
			}
		}
		$return->addLabel(new htmlOutputText(_('Recipient')));
		$return->addField(new htmlOutputText($recipient));
		$return->add(new htmlHiddenInput('recipient', $recipient));
		// new mail alias
		$return->add(new htmlSubTitle(_('Create new alias')));
		$typeManager = new TypeManager();
		$mailAliasTypes = $typeManager->getConfiguredTypesForScope('mailAlias');
		$ous = [];
		foreach ($mailAliasTypes as $type) {
			$ous = array_merge($ous, $type->getSuffixList());
		}
		$ous = array_unique($ous);
		usort($ous, compareDN(...));
		$suffixSelect = new htmlResponsiveSelect('new_ou', $ous, [], _('Suffix'), 'suffix');
		$suffixSelect->setRightToLeftTextDirection(true);
		$suffixSelect->setSortElements(false);
		$return->add($suffixSelect);
		$newAliasCn = empty($_POST['new_cn']) ? '' : $_POST['new_cn'];
		$return->add(new htmlResponsiveInputField(_('Alias name'), 'new_cn', $newAliasCn, 'newAlias'));
		$return->addVerticalSpacer('1rem');
		$addButton = new htmlAccountPageButton(static::class, 'attributes', 'create', _('Create'));
		$return->add($addButton, 12, 12, 12, 'text-center');

		$return->addVerticalSpacer('2rem');

		// add to existing alias
		$return->add(new htmlSubTitle(_('Add to existing alias')));
		$aliasesToAdd = [];
		foreach ($aliases as $index => $attrs) {
			if (!empty($attrs['rfc822mailmember']) && in_array($recipient, $attrs['rfc822mailmember'])) {
				continue;
			}
			$aliasesToAdd[$attrs['cn'][0]] = $index;
		}
		$aliasSelect = new htmlResponsiveSelect('ex_cn', $aliasesToAdd, [], _('Alias names'), 'aliasNames', 20);
		$aliasSelect->setHasDescriptiveElements(true);
		$aliasSelect->setMultiSelect(true);
		$return->add($aliasSelect);
		$return->addVerticalSpacer('1rem');
		$addButton = new htmlAccountPageButton(static::class, 'attributes', 'recipient', _('Add'));
		$return->add($addButton, 12, 12, 12, 'text-center');

		$return->addVerticalSpacer('2rem');
		$cancelButton = new htmlAccountPageButton(static::class, 'attributes', 'back', _('Cancel'));
		$return->add($cancelButton);
		return $return;
	}

	/**
	 * Processes user input of the primary module page.
	 * It checks if all input values are correct and updates the associated LDAP attributes.
	 *
	 * @return array<string|string[]>[] list of info/error messages
	 */
	public function process_add(): array {
		$errors = [];
		// create new alias entry
		if (isset($_POST['form_subpage_' . static::class . '_attributes_create'])) {
			if (empty($_POST['new_cn']) || !get_preg($_POST['new_cn'], 'nis_alias')) {
				$errors[] = $this->messages['alias'][0];
			}
			else {
				// build new alias entry
				$newDN = 'cn=' . $_POST['new_cn'] . ',' . $_POST['new_ou'];
				$found = false;
				foreach ($this->cachedAliasList as $attrs) {
					if ($attrs['dn'] == $newDN) {
						$found = true;
						break;
					}
				}
				if ($found) {
					$errors[] = $this->messages['alias'][1];
				}
				else {
					$newAttrs = [
						'dn' => $newDN,
						'cn' => [$_POST['new_cn']],
						'objectclass' => ['nisMailAlias'],
						'rfc822mailmember' => [$_POST['recipient']]
					];
					$this->aliasesToAdd[$newDN] = $newAttrs;
					$this->cachedAliasList[] = $newAttrs;
				}
			}
		}
		// add recipient to existing entries
		if (isset($_POST['form_subpage_' . static::class . '_attributes_recipient'])) {
			$selectedAliases = $_POST['ex_cn'];
			foreach ($selectedAliases as $index) {
				$dn = $this->cachedAliasList[$index]['dn'];
				$this->cachedAliasList[$index]['rfc822mailmember'][] = $_POST['recipient'];
				if (isset($this->aliasesToAdd[$dn])) {
					$this->aliasesToAdd[$dn]['rfc822mailmember'][] = $_POST['recipient'];
				}
				else {
					$this->recipientsToAdd[$dn][] = $_POST['recipient'];
				}
			}
		}

		return $errors;
	}

	/**
	 * {@inheritDoc}
	 */
	public function postModifyActions(bool $newAccount, array $attributes): array {
		$errors = [];
		// delete complete aliases
		foreach ($this->aliasesToDelete as $dn) {
			$success = @ldap_delete($_SESSION['ldap']->server(), $dn);
			if (!$success) {
				logNewMessage(LOG_ERR, 'Unable to delete ' . $dn . ' (' . ldap_error($_SESSION['ldap']->server()) . ').');
				$errors[] = ['ERROR', sprintf(_('Was unable to delete DN: %s.'), $dn), getDefaultLDAPErrorString($_SESSION['ldap']->server())];
			}
			else {
				logNewMessage(LOG_NOTICE, 'Removed ' . $dn);
			}
		}
		// delete recipient entries
		foreach ($this->recipientsToDelete as $dn => $recipients) {
			$success = @ldap_mod_del($_SESSION['ldap']->server(), $dn, ['rfc822mailmember' => $recipients]);
			if (!$success) {
				logNewMessage(LOG_ERR, 'Unable to remove recipients ' . implode(', ', $recipients) . ' from  ' . $dn . ' (' . ldap_error($_SESSION['ldap']->server()) . ').');
				$errors[] = ['ERROR', sprintf(_('Was unable to remove attributes from DN: %s.'), $dn), getDefaultLDAPErrorString($_SESSION['ldap']->server())];
			}
			else {
				logNewMessage(LOG_NOTICE, 'Removed recipients ' . implode(', ', $recipients) . ' from ' . $dn);
			}
		}
		// create new aliases
		foreach ($this->aliasesToAdd as $dn => $attrs) {
			unset($attrs['dn']);
			$success = @ldap_add($_SESSION['ldap']->server(), $dn, $attrs);
			if (!$success) {
				logNewMessage(LOG_ERR, 'Unable to create mail alias ' . $dn . ' (' . ldap_error($_SESSION['ldap']->server()) . ').');
				$errors[] = ['ERROR', sprintf(_('Was unable to create DN: %s.'), $dn), getDefaultLDAPErrorString($_SESSION['ldap']->server())];
			}
			else {
				logNewMessage(LOG_NOTICE, 'Added mail alias with recipients ' . implode(', ', $attrs['rfc822mailmember']) . ' and DN ' . $dn);
			}
		}
		// add recipients
		foreach ($this->recipientsToAdd as $dn => $recipients) {
			$success = @ldap_mod_add($_SESSION['ldap']->server(), $dn, ['rfc822mailmember' => $recipients]);
			if (!$success) {
				logNewMessage(LOG_ERR, 'Unable to add recipients ' . implode(', ', $recipients) . ' to  ' . $dn . ' (' . ldap_error($_SESSION['ldap']->server()) . ').');
				$errors[] = ['ERROR', sprintf(_('Was unable to add attributes to DN: %s.'), $dn), getDefaultLDAPErrorString($_SESSION['ldap']->server())];
			}
			else {
				logNewMessage(LOG_NOTICE, 'Added recipients ' . implode(', ', $recipients) . ' to ' . $dn);
			}
		}
		return $errors;
	}

	/**
	 * {@inheritDoc}
	 */
	public function get_pdfEntries(array $pdfKeys, string $typeId): array {
		$return = [];
		$aliases = $this->getMailAliasList();
		$foundAliases = [];
		$mails = $this->getMailAddresses();
		$user = $this->getUserName();
		foreach ($aliases as $alias) {
			if (empty($alias['rfc822mailmember'][0])) {
				continue;
			}
			if (!empty($user) && in_array($user, $alias['rfc822mailmember'])) {
				$foundAliases[] = $alias['cn'][0];
			}
			if (!empty($mails)) {
				foreach ($mails as $mail) {
					if (in_array($mail, $alias['rfc822mailmember'])) {
						$foundAliases[] = $alias['cn'][0];
					}
				}
			}
		}
		$foundAliases = array_unique($foundAliases);
		$this->addPDFKeyValue($return, 'alias', _('Alias names'), implode(', ', $foundAliases));
		return $return;
	}

	/**
	 * {@inheritDoc}
	 */
	public function get_configOptions(array $scopes, array $allScopes): htmlElement|array {
		$configContainer = new htmlResponsiveRow();
		$hiddenGroup = new htmlGroup();
		$hiddenGroup->addElement(new htmlOutputText(_('Hidden options')));
		$hiddenGroup->addElement(new htmlHelpLink('hiddenOptions'));
		$configContainer->addLabel($hiddenGroup);
		$configContainer->addField(new htmlOutputText(''));
		$configContainer->addVerticalSpacer('0.5rem');
		$configContainer->add(new htmlResponsiveInputCheckbox('nisMailAliasUser_hideUserAliases', false, _('Aliases for user name')));
		$configContainer->add(new htmlResponsiveInputCheckbox('nisMailAliasUser_hideMailAliases', false, _('Aliases for email')));
		return $configContainer;
	}

	/**
	 * Returns a list of existing email aliases.
	 *
	 * @return array<string, string[]|string>[] email aliases
	 */
	private function getMailAliasList(): array {
		if ($this->cachedAliasList !== null) {
			return $this->cachedAliasList;
		}
		$this->cachedAliasList = searchLDAPByAttribute('cn', '*', 'nisMailAlias', ['dn', 'cn', 'rfc822MailMember'], ['mailAlias']);
		return $this->cachedAliasList;
	}

	/**
	 * Returns the username of this account.
	 *
	 * @return string|null user name
	 */
	private function getUserName() {
		if ($this->getAccountContainer()->getAccountModule('posixAccount') != null) {
			$attrs = $this->getAccountContainer()->getAccountModule('posixAccount')->getAttributes();
			if (!empty($attrs['uid'][0])) {
				return $attrs['uid'][0];
			}
		}
		elseif ($this->getAccountContainer()->getAccountModule('inetOrgPerson') != null) {
			$attrs = $this->getAccountContainer()->getAccountModule('inetOrgPerson')->getAttributes();
			if (!empty($attrs['uid'][0])) {
				return $attrs['uid'][0];
			}
		}
		return null;
	}

	/**
	 * Returns the email addresses of this account.
	 *
	 * @return string[]|null mail addresses
	 */
	private function getMailAddresses() {
		if ($this->getAccountContainer()->getAccountModule('inetOrgPerson') != null) {
			$attrs = $this->getAccountContainer()->getAccountModule('inetOrgPerson')->getAttributes();
			if (!empty($attrs['mail'])) {
				return $attrs['mail'];
			}
		}
		return null;
	}

	/**
	 * Returns if the mail alias type is active. Otherwise, aliases cannot be managed.
	 *
	 * @return boolean is active
	 */
	private function isMailAliasTypeActive() {
		$typeManager = new TypeManager();
		$activeTypes = $typeManager->getConfiguredTypesForScope('mailAlias');
		return !empty($activeTypes);
	}

}
