<?php
/**
 * EGroupware Api: generic base class for SMTP configuration via LDAP
 *
 * @link http://www.stylite.de
 * @package api
 * @subpackage mail
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
 * @copyright (c) 2010-16 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
 * @version $Id$
 */

namespace EGroupware\Api\Mail\Smtp;

use EGroupware\Api;
use EGroupware\Api\Mail;

/**
 * Generic base class for SMTP configuration via LDAP
 *
 * This class uses just inetOrgPerson schema to store primary mail address and aliases
 *
 * Aliases are stored as aditional mail Attributes. The primary mail address is the first one.
 * This schema does NOT support forwarding or disabling of an account for mail.
 *
 * Aliases, forwards, forward-only and quota attribute can be stored in same multivalued attribute
 * with different prefixes.
 *
 * Please do NOT copy this class! Extend it and set the constants different.
 *
 * Please note: schema names muse use correct case (eg. "inetOrgPerson"),
 * while attribute name muse use lowercase, as LDAP returns them as keys in lowercase!
 */
class Ldap extends Mail\Smtp
{
	/**
	 * Name of schema, has to be in the right case!
	 */
	const SCHEMA = 'inetOrgPerson';

	/**
	 * Filter for users
	 */
	const USER_FILTER = '(objectClass=posixAccount)';

	/**
	 * Name of schema for groups, has to be in the right case!
	 */
	const GROUP_SCHEMA = 'posixGroup';

	/**
	 * Attribute with mail address(es)
	 */
	const MAIL_ATTR = 'mail';

	/**
	 * Attribute to enable mail for an account, OR false if existence of ALIAS_ATTR is enough for mail delivery
	 */
	const MAIL_ENABLE_ATTR = false;

	/**
	 * Value for MAIL_ENABLED to use local mail address
	 */
	const MAIL_ENABLED_USE_MAIL = '@mail';

	/**
	 * Attribute for aliases OR false to use mail
	 */
	const ALIAS_ATTR = false;

	/**
	 * Caseinsensitive prefix for aliases (eg. "smtp:"), aliases get added with it and only aliases with it are reported
	 */
	const ALIAS_PREFIX = '';

	/**
	 * Primary mail address required as an alias too: true or false
	 */
	const REQUIRE_MAIL_AS_ALIAS = false;

	/**
	 * Attribute for forwards OR false if not possible
	 */
	const FORWARD_ATTR = false;

	/**
	 * Check if server really supports FORWARD_ATTR and dont write it if not
	 */
	const CHECK_FORWARD_ATTR = false;

	/**
	 * Caseinsensitive prefix for forwards (eg. "forward:"), forwards get added with it and only forwards with it are reported
	 */
	const FORWARD_PREFIX = '';

	/**
	 * Attribute to only forward mail, OR false if not available
	 *
	 * if FORWARD_ONLY_ATTR attribute is identical to FORWARD_ATTR, value of MAIL_ATTR must be added to FORWARD_ONLY_ATTR for local delivery!
	 */
	const FORWARD_ONLY_ATTR = false;

	/**
	 * Value of forward-only attribute, if empty any value will switch forward only on (checked with =*)
	 */
	const FORWARD_ONLY = 'forwardOnly';

	/**
	 * Attribute for mailbox, to which mail gets delivered OR false if not supported
	 */
	const MAILBOX_ATTR = false;

	/**
	 * Attribute for quota limit of user in MB
	 */
	const QUOTA_ATTR = false;

	/**
	 * Caseinsensitive prefix for quota (eg. "quota:"), quota get added with it and only quota with it are reported
	 */
	const QUOTA_PREFIX = '';

	/**
	 * Internal quota in MB is multiplicated with this factor before stored in LDAP
	 */
	const QUOTA_FACTOR = 1048576;

	/**
	 * Attribute for user name
	 */
	const USER_ATTR = 'uid';

	/**
	 * Attribute for numeric user id (optional)
	 */
	const USERID_ATTR = 'uidnumber';

	/**
	 * Base for all searches, defaults to $GLOBALS['egw_info']['server']['ldap_context'] and can be set via setBase($base)
	 *
	 * @var string
	 */
	protected $search_base;

	/**
	 * Special search filter for getUserData only
	 *
	 * @var string
	 */
	protected  $search_filter;

	/**
	 * Log all LDAP writes / actions to error_log
	 */
	var $debug = false;

	/**
	 * from here on implementation, please do NOT copy but extend it!
	 */

	/**
	 * Constructor
	 *
	 * @param string $defaultDomain =null
	 */
	function __construct($defaultDomain=null)
	{
		parent::__construct($defaultDomain);

		if (empty($this->search_base))
		{
			$this->setBase($GLOBALS['egw_info']['server']['ldap_context']);
		}
	}

	/**
	 * Return description for EMailAdmin
	 *
	 * @return string
	 */
	public static function description()
	{
		return 'LDAP ('.static::SCHEMA.')';
	}

	/**
	 * Set ldap search filter for aliases and forwards (getUserData)
	 *
	 * @param string $filter
	 */
	function setFilter($filter)
	{
		$this->search_filter = $filter;
	}

	/**
	 * Set ldap search base, default $GLOBALS['egw_info']['server']['ldap_context']
	 *
	 * @param string $base
	 */
	function setBase($base)
	{
		$this->search_base = $base;
	}

	/**
	 * Hook called on account creation
	 *
	 * @param array $_hookValues values for keys 'account_email', 'account_firstname', 'account_lastname', 'account_lid'
	 * @return boolean true on success, false on error writing to ldap
	 */
	function addAccount($_hookValues)
	{
		$mailLocalAddress = $_hookValues['account_email'] ? $_hookValues['account_email'] :
			Api\Accounts::email($_hookValues['account_firstname'],
				$_hookValues['account_lastname'],$_hookValues['account_lid'],$this->defaultDomain);

		$ds = $this->getLdapConnection();

		$filter = static::USER_ATTR."=".Api\Ldap::quote($_hookValues['account_lid']);

		if (!($sri = @ldap_search($ds, $this->search_base, $filter)))
		{
			return false;
		}
		$allValues 	= ldap_get_entries($ds, $sri);
		$accountDN 	= $allValues[0]['dn'];
		$objectClasses	= $allValues[0]['objectclass'];
		unset($objectClasses['count']);

		// add our mail schema, if not already set
		if(!in_array(static::SCHEMA,$objectClasses) && !in_array(strtolower(static::SCHEMA),$objectClasses))
		{
			$objectClasses[]	= static::SCHEMA;
		}
		// the new code for postfix+cyrus+ldap
		$newData = array(
			static::MAIL_ATTR => $mailLocalAddress,
			'objectclass'	  => $objectClasses
		);
		// does schema have explicit alias attribute AND require mail added as alias too
		if (static::ALIAS_ATTR && static::REQUIRE_MAIL_AS_ALIAS)
		{
			$newData[static::ALIAS_ATTR] = static::ALIAS_PREFIX.$mailLocalAddress;
		}
		// does schema support enabling/disabling mail via attribute
		if (static::MAIL_ENABLE_ATTR)
		{
			$newData[static::MAIL_ENABLE_ATTR] = static::MAIL_ENABLED == self::MAIL_ENABLED_USE_MAIL ?
				$mailLocalAddress : static::MAIL_ENABLED;
		}
		// does schema support an explicit mailbox name --> set it
		if (static::MAILBOX_ATTR && !empty($mailbox = self::mailbox_addr($_hookValues)))
		{
			$newData[static::MAILBOX_ATTR] = $mailbox;
		}

		// allow extending classes to add extra data
		$this->addAccountExtra($_hookValues, $allValues[0], $newData);

		$ret = false;
		try {
			if (isset($newData['mail']) && !is_array($newData['mail']))
			{
				$newData['mail'] = (array)$newData['mail'];
			}
			if (!($ret = ldap_mod_replace($ds, $accountDN, $newData)) || $this->debug)
			{
				error_log(__METHOD__ . '(' . array2string(func_get_args()) . ") --> ldap_mod_replace(,'$accountDN'," .
					array2string($newData) . ') returning ' . array2string($ret) .
					(!$ret ? ' (' . ldap_error($ds) . ')' : ''));
			}
		}
		catch (\Throwable $e) {
			// log but ignore exception and return false
			_egw_log_exception(new \Exception("ldap_mod_replace(\$ds, '$accountDN', ".json_encode($newData).") ".
				$e->getMessage(), $e->getCode(), $e));
		}
		return $ret;
	}

	/**
	 * Add additional values to addAccount and setUserData ($_hookValues['location'])
	 *
	 * @param array $_hookValues
	 * @param array $allValues existing data of account as returned by ldap query
	 * @param array $newData data to update
	 */
	function addAccountExtra(array $_hookValues, array $allValues, array &$newData)
	{
		unset($_hookValues, $allValues, $newData);	// not used, but required by function signature
	}

	/**
	 * Get all email addresses of an account
	 *
	 * @param string $_accountName
	 * @return array
	 */
	function getAccountEmailAddress($_accountName)
	{
		$emailAddresses	= array();
		$ds = $this->getLdapConnection();
		$filter = '(&'.static::USER_FILTER.'('.static::USER_ATTR.'='.Api\Ldap::quote($_accountName).'))';
		$attributes	= array('dn', static::MAIL_ATTR, static::ALIAS_ATTR);
		$sri = @ldap_search($ds, $this->search_base, $filter, $attributes);

		if ($sri)
		{
			$realName = trim($GLOBALS['egw_info']['user']['account_firstname'] . (!empty($GLOBALS['egw_info']['user']['account_firstname']) ? ' ' : '') . $GLOBALS['egw_info']['user']['account_lastname']);
			$allValues = ldap_get_entries($ds, $sri);

			if(isset($allValues[0][static::MAIL_ATTR]))
			{
				foreach($allValues[0][static::MAIL_ATTR] as $key => $value)
				{
					if ($key === 'count') continue;

					$emailAddresses[] = array (
						'name'		=> $realName,
						'address'	=> $value,
						'type'		=> !$key ? 'default' : 'alternate',
					);
				}
			}
			if (static::ALIAS_ATTR && isset($allValues[0][static::ALIAS_ATTR]))
			{
				foreach(self::getAttributePrefix($allValues[0][static::ALIAS_ATTR], static::ALIAS_PREFIX) as $value)
				{
					$emailAddresses[] = array(
						'name'		=> $realName,
						'address'	=> $value,
						'type'		=> 'alternate'
					);
				}
			}
		}
		if ($this->debug) error_log(__METHOD__."('$_accountName') returning ".array2string($emailAddresses));

		return $emailAddresses;
	}

	/**
	 * Get the data of a given user
	 *
	 * Multiple accounts may match, if an email address is specified.
	 * In that case only mail routing fields "uid", "mailbox" and "forward" contain values
	 * from all accounts!
	 *
	 * @param int|string $user numerical account-id, account-name or email address
	 * @param boolean $match_uid_at_domain =true true: uid@domain matches, false only an email or alias address matches
	 * @return array with values for keys 'mailLocalAddress', 'mailAlternateAddress' (array), 'mailForwardingAddress' (array),
	 * 	'accountStatus' ("active"), 'quotaLimit' and 'deliveryMode' ("forwardOnly")
	 */
	function getUserData($user, $match_uid_at_domain=false)
	{
		$userData = array(
			'mailbox' => array(),
			'forward' => array(),

		);

		$ldap = $this->getLdapConnection();

		if (is_numeric($user) && static::USERID_ATTR)
		{
			$filter = '('.static::USERID_ATTR.'='.(int)$user.')';
		}
		elseif (strpos($user, '@') === false)
		{
			if (is_numeric($user)) $user = $GLOBALS['egw']->accounts->id2name($user);
			$filter = '(&'.static::USER_FILTER.'('.static::USER_ATTR.'='.Api\Ldap::quote($user).'))';
		}
		else	// email address --> build filter by attributes defined in config
		{
			list($namepart, $domain) = explode('@', $user);
			if (!empty($this->search_filter))
			{
				$filter = strtr($this->search_filter, array(
					'%s' => Api\Ldap::quote($user),
					'%u' => Api\Ldap::quote($namepart),
					'%d' => Api\Ldap::quote($domain),
				));
			}
			else
			{
				$to_or = array('('.static::MAIL_ATTR.'='.Api\Ldap::quote($user).')');
				if ($match_uid_at_domain) $to_or[] = '('.static::USER_ATTR.'='.Api\Ldap::quote($namepart).')';
				if (static::ALIAS_ATTR)
				{
					$to_or[] = '('.static::ALIAS_ATTR.'='.static::ALIAS_PREFIX.Api\Ldap::quote($user).')';
				}
				$filter = count($to_or) > 1 ? '(|'.implode('', $to_or).')' : $to_or[0];

				// if an enable attribute is set, only return enabled accounts
				if (static::MAIL_ENABLE_ATTR)
				{
					$filter = '(&('.static::MAIL_ENABLE_ATTR.'='.
						(static::MAIL_ENABLED ? static::MAIL_ENABLED : '*').")$filter)";
				}
			}
		}
		$attributes = array_values(array_diff(array(
			static::MAIL_ATTR, 'objectclass', static::USER_ATTR, static::MAIL_ENABLE_ATTR, static::ALIAS_ATTR,
			static::MAILBOX_ATTR, static::FORWARD_ATTR, static::FORWARD_ONLY_ATTR, static::QUOTA_ATTR,
		), array(false, '')));

		$sri = ldap_search($ldap, $this->search_base, $filter, $attributes);

		if ($sri)
		{
			$allValues = ldap_get_entries($ldap, $sri);
			if ($this->debug) error_log(__METHOD__."('$user') --> ldap_search(, '$this->search_base', '$filter') --> ldap_get_entries=".array2string($allValues[0]));

			foreach($allValues as $key => $values)
			{
				if ($key === 'count') continue;

				// groups are always active (if they have an email) and allways forwardOnly
				if (in_array(static::GROUP_SCHEMA, $values['objectclass']))
				{
					$accountStatus = Mail\Smtp::MAIL_ENABLED;
					$deliveryMode = Mail\Smtp::FORWARD_ONLY;
				}
				else	// for users we have to check the attributes
				{
					if (static::MAIL_ENABLE_ATTR)
					{
						$accountStatus = isset($values[static::MAIL_ENABLE_ATTR]) &&
							(static::MAIL_ENABLED === self::MAIL_ENABLED_USE_MAIL && !empty($values[static::MAIL_ENABLE_ATTR][0]) ||
							static::MAIL_ENABLED && !strcasecmp($values[static::MAIL_ENABLE_ATTR][0], static::MAIL_ENABLED) ||
							!static::MAIL_ENABLED && $values[static::ALIAS_ATTR ? static::ALIAS_ATTR : static::MAIL_ATTR]['count'] > 0) ?
								Mail\Smtp::MAIL_ENABLED : '';
					}
					else
					{
						$accountStatus = $values[static::ALIAS_ATTR ? static::ALIAS_ATTR : static::MAIL_ATTR]['count'] > 0 ?
							Mail\Smtp::MAIL_ENABLED : '';
					}
					if (static::FORWARD_ONLY_ATTR)
					{
						// Univention has no extra forward-only attr, but requires primary mail to be in forwards, to have also local delivery
						if (static::FORWARD_ONLY_ATTR === static::FORWARD_ATTR && !static::FORWARD_PREFIX)
						{
							$deliveryMode = '';
							if ($values[static::FORWARD_ATTR])
							{
								if (($k = array_search($values[static::MAIL_ATTR][0], $values[static::FORWARD_ATTR])) !== false)
								{
									unset($values[static::FORWARD_ATTR][$k]);
									$values[static::FORWARD_ATTR]['count']--;
								}
								else
								{
									$deliveryMode = Mail\Smtp::FORWARD_ONLY;
								}
							}
						}
						elseif (static::FORWARD_ONLY)	// check caseinsensitiv for existence of that value
						{
							$deliveryMode = self::getAttributePrefix($values[static::FORWARD_ONLY_ATTR], static::FORWARD_ONLY) ?
								Mail\Smtp::FORWARD_ONLY : '';
						}
						else	// check for existence of any value
						{
							$deliveryMode = $values[static::FORWARD_ONLY_ATTR]['count'] > 0 ?
								Mail\Smtp::FORWARD_ONLY : '';
						}
					}
					else
					{
						$deliveryMode = '';
					}
				}

				// collect mail routing data (can be from multiple (active) accounts and groups!)
				if ($accountStatus)
				{
					// groups never have a mailbox, accounts can have a deliveryMode of "forwardOnly"
					if ($deliveryMode != Mail\Smtp::FORWARD_ONLY)
					{
						$userData[static::USER_ATTR][] = $values[static::USER_ATTR][0];
						if (static::MAILBOX_ATTR && isset($values[static::MAILBOX_ATTR]))
						{
							$userData['mailbox'][] = $values[static::MAILBOX_ATTR][0];
						}
					}
					if (static::FORWARD_ATTR && $values[static::FORWARD_ATTR])
					{
						$userData['forward'] = array_merge($userData['forward'],
							self::getAttributePrefix($values[static::FORWARD_ATTR], static::FORWARD_PREFIX, false));
					}
				}

				// regular user-data can only be from users, NOT groups
				if (in_array(static::GROUP_SCHEMA, $values['objectclass'])) continue;

				$userData['mailLocalAddress'] = $values[static::MAIL_ATTR][0];
				$userData['accountStatus'] = $accountStatus;

				if (static::ALIAS_ATTR)
				{
					$userData['mailAlternateAddress'] = self::getAttributePrefix($values[static::ALIAS_ATTR], static::ALIAS_PREFIX);
				}
				else
				{
					$userData['mailAlternateAddress']	= (array)$values[static::MAIL_ATTR];
					unset($userData['mailAlternateAddress']['count']);
					unset($userData['mailAlternateAddress'][0]);
					$userData['mailAlternateAddress']	= array_values($userData['mailAlternateAddress']);
				}

				if (static::FORWARD_ATTR)
				{
					// remember if server returned forward-attribute
					$this->forward_attr_returned = isset($values[static::FORWARD_ATTR]);

					$userData['mailForwardingAddress']	= self::getAttributePrefix($values[static::FORWARD_ATTR], static::FORWARD_PREFIX);
				}

				if (static::MAILBOX_ATTR) $userData['mailMessageStore']	= $values[static::MAILBOX_ATTR][0];

				$userData['deliveryMode'] = $deliveryMode;

				// eg. suse stores all email addresses as aliases
				if (static::REQUIRE_MAIL_AS_ALIAS &&
					($k = array_search($userData['mailLocalAddress'],$userData['mailAlternateAddress'])) !== false)
				{
					unset($userData['mailAlternateAddress'][$k]);
					$userData['mailAlternateAddress'] = array_values($userData['mailAlternateAddress']);
				}

				if (static::QUOTA_ATTR && isset($values[static::QUOTA_ATTR]))
				{
					$userData['quotaLimit'] = self::getAttributePrefix($values[static::QUOTA_ATTR], static::QUOTA_PREFIX);
					$userData['quotaLimit'] = array_shift($userData['quotaLimit']);
					$userData['quotaLimit'] = $userData['quotaLimit'] ? $userData['quotaLimit'] / static::QUOTA_FACTOR : null;
				}
			}
		}
		if ($this->debug) error_log(__METHOD__."('$user') returning ".array2string($userData));

		return $userData;
	}

	/**
	 * Set the data of a given user
	 *
	 * @param int $_uidnumber numerical user-id
	 * @param array $_mailAlternateAddress
	 * @param array $_mailForwardingAddress
	 * @param string $_deliveryMode
	 * @param string $_accountStatus
	 * @param string $_mailLocalAddress
	 * @param int $_quota in MB
	 * @param boolean $_forwarding_only =false not used as we have our own addAccount method
	 * @param string $_setMailbox =null used only for account migration
	 * @return boolean true on success, false on error writing to ldap
	 */
	function setUserData($_uidnumber, array $_mailAlternateAddress, array $_mailForwardingAddress, $_deliveryMode,
		$_accountStatus, $_mailLocalAddress, $_quota, $_forwarding_only=false, $_setMailbox=null)
	{
		unset($_forwarding_only);	// not used

		if (static::USERID_ATTR)
		{
			$filter = static::USERID_ATTR.'='.(int)$_uidnumber;
		}
		else
		{
			$uid = $GLOBALS['egw']->accounts->id2name($_uidnumber);
			$filter = static::USER_ATTR.'='.Api\Ldap::quote($uid);
		}
		$ldap = $this->getLdapConnection();

		if (!($sri = @ldap_search($ldap, $this->search_base, $filter)))
		{
			return false;
		}
		$allValues 	= ldap_get_entries($ldap, $sri);

		$accountDN 	= $allValues[0]['dn'];
		$uid	   	= $allValues[0][static::USER_ATTR][0];
		$objectClasses	= $allValues[0]['objectclass'];

		unset($objectClasses['count']);

		if(!in_array(static::SCHEMA,$objectClasses) && !in_array(strtolower(static::SCHEMA),$objectClasses))
		{
			$objectClasses[]	= static::SCHEMA;
			$newData['objectclass']	= $objectClasses;
		}

		sort($_mailAlternateAddress);
		sort($_mailForwardingAddress);

		$newData[static::MAIL_ATTR] = $_mailLocalAddress;
		// does schema have explicit alias attribute
		if (static::ALIAS_ATTR)
		{
			self::setAttributePrefix($newData[static::ALIAS_ATTR], $_mailAlternateAddress, static::ALIAS_PREFIX);

			// all email must be stored as alias for suse
			if (static::REQUIRE_MAIL_AS_ALIAS && !in_array($_mailLocalAddress,(array)$_mailAlternateAddress))
			{
				self::setAttributePrefix($newData[static::ALIAS_ATTR], $_mailLocalAddress, static::ALIAS_PREFIX);
			}
		}
		// or de we add them - if existing - to mail attr
		elseif ($_mailAlternateAddress)
		{
			self::setAttributePrefix($newData[static::MAIL_ATTR], $_mailAlternateAddress, static::ALIAS_PREFIX);
		}
		// does schema support to store forwards
		if (static::FORWARD_ATTR)
		{
			// Univention enabled extra local delivery, by adding primary address to forwards
			if (static::FORWARD_ATTR === static::FORWARD_ONLY_ATTR && $_mailForwardingAddress && !$_deliveryMode)
			{
				$_mailForwardingAddress[] = $_mailLocalAddress;
			}
			self::setAttributePrefix($newData[static::FORWARD_ATTR], $_mailForwardingAddress, static::FORWARD_PREFIX);
		}
		// does schema support only forwarding incomming mail
		if (static::FORWARD_ONLY_ATTR && static::FORWARD_ATTR !== static::FORWARD_ONLY_ATTR)
		{
			self::setAttributePrefix($newData[static::FORWARD_ONLY_ATTR],
				$_deliveryMode ? (static::FORWARD_ONLY ? static::FORWARD_ONLY : 'forwardOnly') : array());
		}
		// does schema support an explicit mailbox name --> set it with $uid@$domain
		if (static::MAILBOX_ATTR && empty($allValues[0][static::MAILBOX_ATTR][0]))
		{
			$newData[static::MAILBOX_ATTR] = $this->mailbox_addr(array(
				'account_id' => $_uidnumber,
				'account_lid' => $uid,
				'account_email' => $_mailLocalAddress,
			));
		}
		if (static::QUOTA_ATTR)
		{
			self::setAttributePrefix($newData[static::QUOTA_ATTR],
				(int)$_quota > 0 ? (int)$_quota*static::QUOTA_FACTOR : array(), static::QUOTA_PREFIX);
		}
		// does schema support enabling/disabling mail via attribute
		if (static::MAIL_ENABLE_ATTR)
		{
			$newData[static::MAIL_ENABLE_ATTR]	= $_accountStatus ?
				(static::MAIL_ENABLED == self::MAIL_ENABLED_USE_MAIL ? $_mailLocalAddress : static::MAIL_ENABLED) : array();
		}
		// if we have no mail-enabled attribute, but require primary mail in aliases-attr
		// we do NOT write aliases, if mail is not enabled
		if (!$_accountStatus && !static::MAIL_ENABLE_ATTR && static::REQUIRE_MAIL_AS_ALIAS)
		{
			$newData[static::ALIAS_ATTR] = array();
		}
		// does schema support an explicit mailbox name --> set it, $_setMailbox is given
		if (static::MAILBOX_ATTR && $_setMailbox)
		{
			$newData[static::MAILBOX_ATTR] = $_setMailbox;
		}

		$this->addAccountExtra(array('location' => 'setUserData'), $allValues[0], $newData);

		if ($this->debug) error_log(__METHOD__.'('.array2string(func_get_args()).") --> ldap_mod_replace(,'$accountDN',".array2string($newData).')');

		if (isset($newData['mail']) && is_array($newData['mail']))
		{
			$newData['mail'] = (array)$newData['mail'];
		}
		if (!($ret = @ldap_mod_replace($ldap, $accountDN, $newData)) && static::CHECK_FORWARD_ATTR)
		{
			unset($newData[static::FORWARD_ATTR]);

			$ret = @ldap_mod_replace($ldap, $accountDN, $newData);
		}
		return $ret;
	}

	/**
	 * Saves the forwarding information
	 *
	 * @param int $_accountID
	 * @param string $_forwardingAddress
	 * @param string $_keepLocalCopy 'yes'
	 * @return boolean true on success, false on error writing to ldap
	 */
	function saveSMTPForwarding($_accountID, $_forwardingAddress, $_keepLocalCopy)
	{
		$ds = $this->getLdapConnection();
		if (static::USERID_ATTR)
		{
			$filter = '(&'.static::USER_FILTER.'('.static::USERID_ATTR.'='.(int)$_accountID.'))';
		}
		else
		{
			$uid = $GLOBALS['egw']->accounts->id2name($_accountID);
			$filter = '(&'.static::USER_FILTER.'('.static::USER_ATTR.'='.Api\Ldap::quote($uid).'))';
		}
		$attributes	= array('dn', static::MAIL_ATTR, static::FORWARD_ATTR, 'objectclass');
		if (static::FORWARD_ONLY_ATTR)
		{
			$attributes[] = static::FORWARD_ONLY_ATTR;
		}
		$sri = ldap_search($ds, $this->search_base, $filter, $attributes);

		if ($sri)
		{
			$newData = array();
			$allValues = ldap_get_entries($ds, $sri);
			$objectClasses  = $allValues[0]['objectclass'];
			$newData['objectclass']	= $allValues[0]['objectclass'];

			unset($newData['objectclass']['count']);

			if(!in_array(static::SCHEMA,$objectClasses))
			{
				$newData['objectclass'][] = static::SCHEMA;
			}
			if (static::FORWARD_ATTR)
			{
				// copy all non-forward data (different prefix) to newData, all existing forwards to $forwards
				$newData[static::FORWARD_ATTR] = $allValues[0][static::FORWARD_ATTR];
				$forwards = self::getAttributePrefix($newData[static::FORWARD_ATTR], static::FORWARD_PREFIX);

				if(!empty($_forwardingAddress))
				{
					if($forwards)
					{
						if (!is_array($_forwardingAddress))
						{
							// replace the first forwarding address (old behavior)
							$forwards[0] = $_forwardingAddress;
						}
						else
						{
							// replace all forwarding Addresses
							$forwards = $_forwardingAddress;
						}
					}
					else
					{
						$forwards = (array)$_forwardingAddress;
					}
					if (static::FORWARD_ONLY_ATTR)
					{
						// Univention adds primary email to forwards to signal local delivery too
						if (static::FORWARD_ONLY_ATTR === static::FORWARD_ATTR)
						{
							if ($_keepLocalCopy == 'yes')
							{
								$forwards[] = $allValues[static::MAIL_ATTR][0];
							}
						}
						else
						{
							self::setAttributePrefix($newData[static::FORWARD_ONLY_ATTR],
								$_keepLocalCopy == 'yes' ? array() : static::FORWARD_ONLY);
						}
					}
				}
				else
				{
					$forwards = array();
				}
				// merge in again all new set forwards incl. opt. prefix
				self::setAttributePrefix($newData[static::FORWARD_ATTR], $forwards, static::FORWARD_PREFIX);
			}
			if ($this->debug) error_log(__METHOD__.'('.array2string(func_get_args()).") --> ldap_mod_replace(,'{$allValues[0]['dn']}',".array2string($newData).')');

			if (isset($newData['mail']) && is_array($newData['mail']))
			{
				$newData['mail'] = (array)$newData['mail'];
			}
			return ldap_modify ($ds, $allValues[0]['dn'], $newData);
		}
	}

	/**
	 * Get configured mailboxes of a domain
	 *
	 * @param boolean $return_inactive return mailboxes NOT marked as accountStatus=active too
	 * @return array uid => name-part of mailMessageStore
	 */
	function getMailboxes($return_inactive)
	{
		$ds = $this->getLdapConnection();
		$filter = array('('.static::MAIL_ATTR.'=*)');
		$attrs = array(static::USER_ATTR, static::MAIL_ATTR);
		if (static::MAILBOX_ATTR)
		{
			$filter[] = '('.static::MAILBOX_ATTR.'=*)';
			$attrs[] = static::MAILBOX_ATTR;
		}
		if (!$return_inactive && static::MAIL_ENABLE_ATTR)
		{
			$filter[] = '('.static::MAIL_ENABLE_ATTR.'='.static::MAIL_ENABLED.')';
		}
		if (count($filter) > 1)
		{
			$filter = '(&'.implode('', $filter).')';
		}
		else
		{
			$filter = $filter[0];
		}
		if (!($sr = @ldap_search($ds, $this->search_base, $filter, $attrs)))
		{
			//error_log("Error ldap_search(\$ds, '$base', '$filter')!");
			return array();
		}
		$entries = ldap_get_entries($ds, $sr);

		unset($entries['count']);

		$mailboxes = array();
		foreach($entries as $entry)
		{
			if ($entry[static::USER_ATTR][0] == 'anonymous') continue;	// anonymous is never a mail-user!
			list($mailbox) = explode('@', $entry[static::MAILBOX_ATTR ? static::MAILBOX_ATTR : static::MAIL_ATTR][0]);
			$mailboxes[$entry[static::USER_ATTR][0]] = $mailbox;
		}
		return $mailboxes;
	}

	/**
	 * Set values in a given LDAP attribute using an optional prefix
	 *
	 * @param array &$attribute on return array with values set and existing values preseved
	 * @param string|array $values value(s) to set
	 * @param string $prefix ='' prefix to use or ''
	 */
	protected static function setAttributePrefix(&$attribute, $values, $prefix='')
	{
		//$attribute_in = $attribute;
		if (empty($attribute)) $attribute = array();
		if (!is_array($attribute)) $attribute = (array)$attribute;

		foreach((array)$values as $value)
		{
			$attribute[] = $prefix.$value;
		}
		//error_log(__METHOD__."(".array2string($attribute_in).", ".array2string($values).", '$prefix') attribute=".array2string($attribute));
	}

	/**
	 * Get values having an optional prefix from a given LDAP attribute
	 *
	 * @param array &$attribute only "count" and prefixed values get removed, get's reindexed, if values have been removed
	 * @param string $prefix ='' prefix to use or ''
	 * @param boolean $remove =true remove returned values from $attribute
	 * @return array with values (prefix removed) or array() if nothing found
	 */
	protected static function getAttributePrefix(&$attribute, $prefix='', $remove=true)
	{
		//$attribute_in = $attribute;
		$values = array();

		if (isset($attribute))
		{
			unset($attribute['count']);

			foreach($attribute as $key => $value)
			{
				if (!$prefix || stripos($value, $prefix) === 0)
				{
					if ($remove) unset($attribute[$key]);
					$values[] = substr($value, strlen($prefix));
				}
			}
			// reindex $attribute, if neccessary
			if ($values && $attribute) $attribute = array_values($attribute);
		}
		//error_log(__METHOD__."(".array2string($attribute_in).", '$prefix', $remove) attribute=".array2string($attribute).' returning '.array2string($values));
		return $values;
	}

	/**
	 * Return LDAP connection
	 */
	protected function getLdapConnection()
	{
		return Api\Ldap::factory();
	}
}