<?php
/**
 * EGroupware EMailAdmin: Wizard to create mail accounts
 *
 * @link http://www.egroupware.org
 * @package emailadmin
 * @author Ralf Becker <rb@egroupware.org>
 * @copyright (c) 2013-18 by Ralf Becker <rb@egroupware.org>
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
 */

use EGroupware\Api;
use EGroupware\Api\Framework;
use EGroupware\Api\Acl;
use EGroupware\Api\Etemplate;
use EGroupware\Api\Mail;
use EGroupware\Api\Auth\OpenIDConnectClient;
use Jumbojett\OpenIDConnectClientException;

/**
 * Wizard to create mail accounts
 *
 * Wizard uses follow heuristic to search for IMAP accounts:
 * 1. query Mozilla ISPDB for domain from email (perfering SSL over STARTTLS over insecure connection)
 * 2. guessing and verifying in DNS server-names based on domain from email:
 *	- (imap|smtp).$domain, mail.$domain
 *  - MX is *.mail.protection.outlook.com use (outlook|smtp).office365.com
 *  - MX for $domain
 *  - replace host in MX with (imap|smtp) or mail
 */
class admin_mail
{
	/**
	 * Enable logging of IMAP communication to given path, eg. /tmp/autoconfig.log
	 */
	const DEBUG_LOG = null; //'/var/lib/egroupware/imap.log';
	/**
	 * Connection timeout in seconds used in autoconfig, can and should be really short!
	 */
	const TIMEOUT = 3;
	/**
	 * Prefix for callback names
	 *
	 * Used as static::APP_CLASS in etemplate::exec(), to allow mail app extending this class.
	 */
	const APP_CLASS = 'admin.admin_mail.';

	/**
	 * 0: No SSL
	 */
	const SSL_NONE = Mail\Account::SSL_NONE;
	/**
	 * 1: STARTTLS on regular tcp connection/port
	 */
	const SSL_STARTTLS = Mail\Account::SSL_STARTTLS;
	/**
	 * 3: SSL (inferior to TLS!)
	 */
	const SSL_SSL = Mail\Account::SSL_SSL;
	/**
	 * 2: require TLS version 1+, no SSL version 2 or 3
	 */
	const SSL_TLS = Mail\Account::SSL_TLS;
	/**
	 * 8: if set, verify certifcate (currently not implemented in Horde_Imap_Client!)
	 */
	const SSL_VERIFY = Mail\Account::SSL_VERIFY;

	/**
	 * Log exception including trace to error-log, instead of just displaying the message.
	 *
	 * @var boolean
	 */
	public static $debug = false;

	/**
	 * Methods callable via menuaction
	 *
	 * @var array
	 */
	public $public_functions = array(
		'add' => true,
		'edit' => true,
		'ajax_activeAccounts' => true
	);

	/**
	 * Supported ssl types including none
	 *
	 * @var array
	 */
	public static $ssl_types = array(
		self::SSL_TLS => 'TLS',	// SSL with minimum TLS (no SSL v.2 or v.3), requires Horde_Imap_Client-2.16.0/Horde_Socket_Client-1.1.0
		self::SSL_SSL => 'SSL',
		self::SSL_STARTTLS => 'STARTTLS',
		'no' => 'no',
	);
	/**
	 * Convert ssl-type to Horde secure parameter
	 *
	 * @var array
	 */
	public static $ssl2secure = array(
		'SSL' => 'ssl',
		'STARTTLS' => 'tls',
		'TLS' => 'tlsv1',	// SSL with minimum TLS (no SSL v.2 or v.3), requires Horde_Imap_Client-2.16.0/Horde_Socket_Client-1.1.0
	);
	/**
	 * Convert ssl-type to eMailAdmin acc_(imap|sieve|smtp)_ssl integer value
	 *
	 * @var array
	 */
	public static $ssl2type = array(
		'TLS' => self::SSL_TLS,
		'SSL' => self::SSL_SSL,
		'STARTTLS' => self::SSL_STARTTLS,
		'no' => self::SSL_NONE,
	);

	/**
	 * Available IMAP login types
	 *
	 * @var array
	 */
	public static $login_types = array(
		'' => 'Username specified below for all',
		'standard'	=> 'username from account',
		'vmailmgr'	=> 'username@domainname',
		//'admin'		=> 'Username/Password defined by admin',
		'uidNumber' => 'UserId@domain eg. u1234@domain',
		'email'	    => 'EMail-address from account',
		'domain/username' => 'Exchange: domain/username',
	);

	/**
	 * Options for further identities
	 *
	 * @var array
	 */
	public static $further_identities = array(
		0 => 'Forbid users to create identities',
		1 => 'Allow users to create further identities',
		2 => 'Allow users to create identities for aliases',
	);

	/**
	 * List of domains know to not support Sieve
	 *
	 * Used to switch Sieve off by default, thought users can always try switching it on.
	 * Testing not existing Sieve with google takes a long time, as ports are open,
	 * but not answering ...
	 *
	 * @var array
	 */
	public static $no_sieve_blacklist = array('gmail.com', 'googlemail.com', 'outlook.office365.com');

	/**
	 * Is current use a mail administrator / has run rights for EMailAdmin
	 *
	 * @var boolean
	 */
	protected $is_admin = false;

	/**
	 * Constructor
	 */
	public function __construct()
	{
		$this->is_admin = isset($GLOBALS['egw_info']['user']['apps']['admin']);

		// for some reason most translation for account-wizard are in mail
		Api\Translation::add_app('mail');

		// Horde use locale for translation of error messages
		Api\Preferences::setlocale(LC_MESSAGES);
	}

	/**
	 * Step 1: IMAP account
	 *
	 * @param array $content
	 * @param string $msg
	 */
	public function add(array $content=array(), $msg='', $msg_type='success')
	{
		$tpl = new Etemplate('admin.mailwizard');
		if (empty($content['account_id']))
		{
			$content['account_id'] = $GLOBALS['egw_info']['user']['account_id'];
		}
		// add some defaults if not already set (+= does not overwrite existing values!)
		$content += array(
			'ident_realname' => $GLOBALS['egw']->accounts->id2name($content['account_id'], 'account_fullname'),
			'ident_email' => $GLOBALS['egw']->accounts->id2name($content['account_id'], 'account_email'),
			'acc_imap_port' => 993,
			'manual_class' => 'emailadmin_manual',
		);
		Framework::message($msg ? $msg : (string)$_GET['msg'], $msg_type);

		if (!empty($content['acc_imap_host']) || !empty($content['acc_imap_username']))
		{
			$readonlys['button[manual]'] = true;
			unset($content['manual_class']);
		}
		$tpl->exec(static::APP_CLASS.'autoconfig', $content, array(
			'acc_imap_ssl' => self::$ssl_types,
		), $readonlys, $content, 2);
	}

	/**
	 * Try to autoconfig an account
	 *
	 * @param array $content
	 */
	public function autoconfig(array $content)
	{
		// user pressed [Skip IMAP] --> jump to SMTP config
		if (!empty($content['button']) && key($content['button']) === 'skip_imap')
		{
			unset($content['button']);
			if (!isset($content['acc_smtp_host'])) $content['acc_smtp_host'] = '';	// do manual mode right away
			return $this->smtp($content, lang('Skipping IMAP configuration!'));
		}
		$tpl = new Etemplate('admin.mailwizard');
		$sel_options = $readonlys = $hosts = [];

		$connected = $content['connected'] ?? null;
		if (empty($content['acc_imap_username']))
		{
			$content['acc_imap_username'] = $content['ident_email'];
		}
		// supported oauth provider or mail-server of them for custom domains
		if (($oauth = OpenIDConnectClient::providerByDomain($content['acc_imap_username'], $content['acc_imap_host'])))
		{
			$content['output'] .= lang('Using IMAP:%1, SMTP:%2, OAUTH:%3:', $oauth['imap'], $oauth['smtp'], $oauth['provider'])."\n";
			$hosts[$oauth['imap']] = true;
			$content += self::oauth2content($oauth);
		}
		elseif (!empty($content['acc_imap_host']))
		{
			$hosts = array($content['acc_imap_host'] => true);
			if ($content['acc_imap_port'] > 0 && !in_array($content['acc_imap_port'], array(143,993)))
			{
				$ssl_type = (string)array_search($content['acc_imap_ssl'], self::$ssl2type);
				if ($ssl_type === '') $ssl_type = 'insecure';
				$hosts[$content['acc_imap_host']] = array(
					$ssl_type => $content['acc_imap_port'],
				);
			}
		}
		elseif (($ispdb = self::mozilla_ispdb($content['ident_email'])) && count($ispdb['imap']))
		{
			$content['ispdb'] = $ispdb;
			$content['output'] .= lang('Using data from Mozilla ISPDB for provider %1', $ispdb['displayName'])."\n";
			$hosts = array();
			foreach($ispdb['imap'] as $server)
			{
				if (!isset($hosts[$server['hostname']]))
				{
					$hosts[$server['hostname']] = array('username' => $server['username']);
				}
				if (strtoupper($server['socketType']) == 'SSL')	// try TLS first
				{
					$hosts[$server['hostname']]['TLS'] = $server['port'];
				}
				$hosts[$server['hostname']][strtoupper($server['socketType'])] = $server['port'];
				// make sure we prefer SSL over STARTTLS over insecure
				if (count($hosts[$server['hostname']]) > 2)
				{
					$hosts[$server['hostname']] = self::fix_ssl_order($hosts[$server['hostname']]);
				}
			}
		}
		else
		{
			$hosts = $this->guess_hosts($content['ident_email'], 'imap');
		}

		// check if support OAuth for that domain or we have a password
		if (empty($oauth) && empty($content['acc_oauth_provider_url']) && empty($content['acc_imap_password']))
		{
			Etemplate::set_validation_error('acc_imap_password', lang('Field must not be empty!'));
			$connected = false;
		}

		// iterate over all hosts and try to connect
		foreach(!isset($connected) ? $hosts : [] as $host => $data)
		{
			// check if we support OAuth for the (manual) configured mail-server
			if (empty($content['acc_oauth_provider_url']) && ($oauth = OpenIDConnectClient::providerByDomain($content['acc_imap_username'], $host)))
			{
				$content += self::oauth2content($oauth);
			}
			$content['acc_imap_host'] = $host;
			// by default we check SSL, STARTTLS and at last an insecure connection
			if (!is_array($data)) $data = array('TLS' => 993, 'SSL' => 993, 'STARTTLS' => 143, 'insecure' => 143);

			foreach($data as $ssl => $port)
			{
				if ($ssl === 'username') continue;

				$content['acc_imap_ssl'] = (int)self::$ssl2type[$ssl];

				$e = null;
				try {
					$content['output'] .= "\n".Api\DateTime::to('now', 'H:i:s').": Trying $ssl connection to $host:$port ...\n";
					$content['acc_imap_port'] = $port;

					$imap = self::imap_client($content, self::TIMEOUT);

					//$content['output'] .= array2string($imap->capability());
					$imap->login();
					$content['output'] .= "\n".lang('Successful connected to %1 server%2.', 'IMAP', ' '.lang('and logged in'))."\n";
					if (!$imap->isSecureConnection())
					{
						$content['output'] .= lang('Connection is NOT secure! Everyone can read eg. your credentials.')."\n";
						$content['acc_imap_ssl'] = 'no';
					}
					//$content['output'] .= "\n\n".array2string($imap->capability());
					$content['connected'] = $connected = true;
					break 2;
				}
				catch(Horde_Imap_Client_Exception $e)
				{
					switch($e->getCode())
					{
						case Horde_Imap_Client_Exception::LOGIN_AUTHENTICATIONFAILED:
							$content['output'] .= "\n".$e->getMessage()."\n";
							break 3;	// no need to try other SSL or non-SSL connections, if auth failed

						case Horde_Imap_Client_Exception::SERVER_CONNECT:
							$content['output'] .= "\n".$e->getMessage()."\n";
							if ($ssl == 'STARTTLS') break 2;	// no need to try insecure connection on same port
							break;

						default:
							$content['output'] .= "\n".get_class($e).': '.$e->getMessage().' ('.$e->getCode().')'."\n";
							//$content['output'] .= $e->getTraceAsString()."\n";
					}
					if (self::$debug) _egw_log_exception($e);
				}
				catch(Exception $e) {
					$content['output'] .= "\n".get_class($e).': '.$e->getMessage().' ('.$e->getCode().')'."\n";
					//$content['output'] .= $e->getTraceAsString()."\n";
					if (self::$debug) _egw_log_exception($e);
				}
			}
		}
		if ($connected)	// continue with next wizard step: define folders
		{
			unset($content['button']);
			return $this->folder($content, lang('Successful connected to %1 server%2.', 'IMAP', ' '.lang('and logged in')).
				($imap->isSecureConnection() ? '' : "\n".lang('Connection is NOT secure! Everyone can read eg. your credentials.')));
		}
		// add validation error, if we can identify a field
		if (!$connected && $e instanceof Horde_Imap_Client_Exception)
		{
			switch($e->getCode())
			{
				case Horde_Imap_Client_Exception::LOGIN_AUTHENTICATIONFAILED:
					Etemplate::set_validation_error('acc_imap_username', lang($e->getMessage()));
					Etemplate::set_validation_error('acc_imap_password', lang($e->getMessage()));
					break;

				case Horde_Imap_Client_Exception::SERVER_CONNECT:
					Etemplate::set_validation_error('acc_imap_host', lang($e->getMessage()));
					break;
			}
		}
		$readonlys['button[manual]'] = true;
		unset($content['manual_class'], $content['button']);
		$sel_options['acc_imap_ssl'] = self::$ssl_types;
		$tpl->exec(static::APP_CLASS.'autoconfig', $content, $sel_options, $readonlys,
			array_diff_key($content, ['output'=>true]), 2);
	}

	/**
	 * Convert OAuth provider data to our content-names
	 *
	 * @param array $oauth
	 * @return array
	 */
	protected static function oauth2content(array $oauth)
	{
		return [
			'acc_smpt_host' => $oauth['smtp'],
			'acc_sieve_enabled' => false,
			'acc_oauth_provider_url' => $oauth['provider'],
			'acc_oauth_client_id' => $oauth['client'],
			'acc_oauth_client_secret' => $oauth['secret'],
			'acc_oauth_scopes' => $oauth['scopes'],
			OpenIDConnectClient::ADD_CLIENT_TO_WELL_KNOWN => $oauth[OpenIDConnectClient::ADD_CLIENT_TO_WELL_KNOWN] ?? null,
			OpenIDConnectClient::ADD_AUTH_PARAM => $oauth[OpenIDConnectClient::ADD_AUTH_PARAM] ?? null,
		];
	}

	/**
	 * Step 2: Folder - let user select trash, sent, drafs and template folder
	 *
	 * @param ?array $content
	 * @param string $msg =''
	 * @param Horde_Imap_Client_Socket $imap =null
	 */
	public function folder(?array $content, $msg='', ?Horde_Imap_Client_Socket $imap=null)
	{
		if (!empty($content['button']))
		{
			$button = key($content['button']);
			unset($content['button']);
			switch($button)
			{
				case 'back':
					return $this->add($content);

				case 'continue':
					return $this->sieve($content);
			}
		}
		$content['msg'] = $msg;
		if (!isset($imap)) $imap = self::imap_client ($content);

		try {
			//_debug_array($content);
			$sel_options['acc_folder_sent'] = $sel_options['acc_folder_trash'] =
				$sel_options['acc_folder_draft'] = $sel_options['acc_folder_template'] =
					$sel_options['acc_folder_junk'] = $sel_options['acc_folder_archive'] =
						$sel_options['acc_folder_ham'] = self::mailboxes($imap, $content);
		}
		catch(Exception $e) {
			$content['msg'] = $e->getMessage();
			if (self::$debug) _egw_log_exception($e);
		}

		$tpl = new Etemplate('admin.mailwizard.folder');
		$tpl->exec(static::APP_CLASS.'folder', $content, $sel_options, array(), $content, 2);
	}

	/**
	 * Query mailboxes and (optional) detect special folders
	 *
	 * @param Horde_Imap_Client_Socket $imap
	 * @param array &$content=null on return values for acc_folder_(sent|trash|draft|template)
	 * @return array with folders as key AND value
	 * @throws Horde_Imap_Client_Exception
	 */
	public static function mailboxes(Horde_Imap_Client_Socket $imap, array &$content=null)
	{
		// query all subscribed mailboxes
		$mailboxes = $imap->listMailboxes('*', Horde_Imap_Client::MBOX_SUBSCRIBED, array(
			'special_use' => true,
			'attributes' => true,	// otherwise special_use is only queried, but not returned ;-)
			'delimiter' => true,
		));
		//_debug_array($mailboxes);
		// list mailboxes by special-use attributes
		$folders = $attributes = $all = array();
		foreach($mailboxes as $mailbox => $data)
		{
			foreach($data['attributes'] as $attribute)
			{
				$attributes[$attribute][] = $mailbox;
			}
			$folders[$mailbox] = $mailbox.': '.implode(', ', $data['attributes']);
		}
		// pre-select send, trash, ... folder for user, by checking special-use attributes or common name(s)
		foreach(array(
			'acc_folder_sent'  => array('\\sent', 'sent'),
			'acc_folder_trash' => array('\\trash', 'trash'),
			'acc_folder_draft' => array('\\drafts', 'drafts'),
			'acc_folder_template' => array('', 'templates'),
			'acc_folder_junk'  => array('\\junk', 'junk', 'spam'),
			'acc_folder_ham'   => array('', 'ham'),
			'acc_folder_archive' => array('', 'archive'),
		) as $name => $common_names)
		{
			unset($content[$name]);
			// first check special-use attributes
			if (($special_use = array_shift($common_names)))
			{
				foreach((array)$attributes[$special_use] as $mailbox)
				{
					if (empty($content[$name]) || is_string($mailbox) && strlen($mailbox) < strlen($content[$name]))
					{
						$content[$name] = $mailbox;
					}
				}
			}
			// no special use folder found, try common names
			if (empty($content[$name]))
			{
				foreach($mailboxes as $mailbox => $data)
				{
					$delimiter = !empty($data['delimiter']) ? $data['delimiter'] : '.';
					$name_parts = explode($delimiter, strtolower($mailbox));
					if (array_intersect($name_parts, $common_names) &&
						(empty($content[$name]) || is_string($mailbox) && strlen($mailbox) < strlen($content[$name]) && substr($content[$name], 0, 6) != 'INBOX'.$delimiter))
					{
						//error_log(__METHOD__."() $mailbox --> ".substr($name, 11).' folder');
						$content[$name] = $mailbox;
					}
					//else error_log(__METHOD__."() $mailbox does NOT match array_intersect(".array2string($name_parts).', '.array2string($common_names).')='.array2string(array_intersect($name_parts, $common_names)));
				}
			}
			$folders[(string)$content[$name]] .= ' --> '.substr($name, 11).' folder';
		}
		// uncomment for infos about selection process
		//$content['folder_output'] = implode("\n", $folders);

		return array_combine(array_keys($mailboxes), array_keys($mailboxes));
	}

	/**
	 * Step 3: Sieve
	 *
	 * @param array $content
	 * @param string $msg =''
	 */
	public function sieve(array $content, $msg='')
	{
		static $sieve_ssl2port = array(
			self::SSL_TLS => 5190,
			self::SSL_SSL => 5190,
			self::SSL_STARTTLS => array(4190, 2000),
			self::SSL_NONE => array(4190, 2000),
		);
		$content['msg'] = $msg;

		if (!empty($content['button']))
		{
			$button = key($content['button']);
			unset($content['button']);
			switch($button)
			{
				case 'back':
					return $this->folder($content);

				case 'continue':
					if (!$content['acc_sieve_enabled'])
					{
						return $this->smtp($content);
					}
					break;
			}
		}
		// first try: hide manual config
		if (!isset($content['acc_sieve_enabled']))
		{
			list(, $domain) = explode('@', $content['acc_imap_username']);
			$content['acc_sieve_enabled'] = (int)!in_array($domain, self::$no_sieve_blacklist);
			$content['manual_class'] = 'emailadmin_manual';
		}
		else
		{
			unset($content['manual_class']);
			$readonlys['button[manual]'] = true;
		}
		// set default ssl and port
		if (!isset($content['acc_sieve_ssl'])) $content['acc_sieve_ssl'] = key(self::$ssl_types);
		if (empty($content['acc_sieve_port'])) $content['acc_sieve_port'] = $sieve_ssl2port[$content['acc_sieve_ssl']];

		// check smtp connection
		if ($button == 'continue')
		{
			$content['sieve_connected'] = false;
			$content['sieve_output'] = '';
			unset($content['manual_class']);

			if (empty($content['acc_sieve_host']))
			{
				$content['acc_sieve_host'] = $content['acc_imap_host'];
			}
			// if use set non-standard port, use it
			if (!in_array($content['acc_sieve_port'], (array)$sieve_ssl2port[$content['acc_sieve_ssl']]))
			{
				$data = array($content['acc_sieve_ssl'] => $content['acc_sieve_port']);
			}
			else	// otherwise try all standard ports
			{
				$data = $sieve_ssl2port;
			}
			foreach($data as $ssl => $ports)
			{
				foreach((array)$ports as $port)
				{
					$content['acc_sieve_ssl'] = $ssl;
					$ssl_label = self::$ssl_types[$ssl];

					$e = null;
					try {
						$content['sieve_output'] .= "\n".Api\DateTime::to('now', 'H:i:s').": Trying $ssl_label connection to $content[acc_sieve_host]:$port ...\n";
						$content['acc_sieve_port'] = $port;
						$sieve = new Horde\ManageSieve(array(
							'host' => $content['acc_sieve_host'],
							'port' => $content['acc_sieve_port'],
							'secure' => self::$ssl2secure[(string)array_search($content['acc_sieve_ssl'], self::$ssl2type)],
							'timeout' => self::TIMEOUT,
							'logger' => self::DEBUG_LOG ? new admin_mail_logger(self::DEBUG_LOG) : null,
						));
						// connect to sieve server
						$sieve->connect();
						$content['sieve_output'] .= "\n".lang('Successful connected to %1 server%2.', 'Sieve','');
						// and log in
						$sieve->login($content['acc_imap_username'], $content['acc_imap_password']);
						$content['sieve_output'] .= ' '.lang('and logged in')."\n";
						$content['sieve_connected'] = true;

						unset($content['button']);
						return $this->smtp($content, lang('Successful connected to %1 server%2.', 'Sieve',
							' '.lang('and logged in')));
					}
					catch(Horde\ManageSieve\Exception\ConnectionFailed $e) {
						$content['sieve_output'] .= "\n".$e->getMessage().' '.$e->details."\n";
					}
					catch(Exception $e) {
						$content['sieve_output'] .= "\n".get_class($e).': '.$e->getMessage().
							($e->details ? ' '.$e->details : '').' ('.$e->getCode().')'."\n";
						$content['sieve_output'] .= $e->getTraceAsString()."\n";
						if (self::$debug) _egw_log_exception($e);
					}
				}
			}
			// not connected, and default ssl/port --> reset again to secure settings
			if ($data == $sieve_ssl2port)
			{
				$content['acc_sieve_ssl'] = key(self::$ssl_types);
				$content['acc_sieve_port'] = $sieve_ssl2port[$content['acc_sieve_ssl']];
			}
		}
		// add validation error, if we can identify a field
		if (!$content['sieve_connected'] && $e instanceof Exception)
		{
			switch($e->getCode())
			{
				case 61:	// connection refused
				case 60:	// connection timed out (imap.googlemail.com returns that for none-ssl/4190/2000)
				case 65:	// no route ot host (imap.googlemail.com returns that for ssl/5190)
					Etemplate::set_validation_error('acc_sieve_host', lang($e->getMessage()));
					Etemplate::set_validation_error('acc_sieve_port', lang($e->getMessage()));
					break;
			}
			$content['msg'] = lang('No sieve support detected, either fix configuration manually or leave it switched off.');
			$content['acc_sieve_enabled'] = 0;
		}
		$sel_options['acc_sieve_ssl'] = self::$ssl_types;
		$tpl = new Etemplate('admin.mailwizard.sieve');
		$tpl->exec(static::APP_CLASS.'sieve', $content, $sel_options, $readonlys, $content, 2);
	}

	/**
	 * Step 4: SMTP
	 *
	 * @param array $content
	 * @param string $msg =''
	 */
	public function smtp(array $content, $msg='')
	{
		static $smtp_ssl2port = array(
			self::SSL_NONE => 25,
			self::SSL_SSL => 465,
			self::SSL_TLS => 465,
			self::SSL_STARTTLS => 587,
		);
		$content['msg'] = $msg;

		if (!empty($content['button']))
		{
			$button = key($content['button']);
			unset($content['button']);
			switch($button)
			{
				case 'back':
					return $this->sieve($content);
			}
		}
		// first try: hide manual config
		if (!isset($content['acc_smtp_host']))
		{
			$content['manual_class'] = 'emailadmin_manual';
		}
		else
		{
			unset($content['manual_class']);
			$readonlys['button[manual]'] = true;
		}
		// copy username/password from imap
		if (!isset($content['acc_smtp_username'])) $content['acc_smtp_username'] = $content['acc_imap_username'];
		if (!isset($content['acc_smtp_password'])) $content['acc_smtp_password'] = $content['acc_imap_password'];
		// set default ssl
		if (!isset($content['acc_smtp_ssl'])) $content['acc_smtp_ssl'] = key(self::$ssl_types);
		if (empty($content['acc_smtp_port'])) $content['acc_smtp_port'] = $smtp_ssl2port[$content['acc_smtp_ssl']];

		// check smtp connection
		if ($button == 'continue')
		{
			$content['smtp_connected'] = false;
			$content['smtp_output'] = '';
			unset($content['manual_class']);

			if (!empty($content['acc_smtp_host']))
			{
				$hosts = array($content['acc_smtp_host'] => true);
				if ((string)$content['acc_smtp_ssl'] !== (string)self::SSL_TLS || $content['acc_smtp_port'] != $smtp_ssl2port[$content['acc_smtp_ssl']])
				{
					$ssl_type = (string)array_search($content['acc_smtp_ssl'], self::$ssl2type);
					$hosts[$content['acc_smtp_host']] = array(
						$ssl_type => $content['acc_smtp_port'],
					);
				}
			}
			elseif($content['ispdb'] && !empty($content['ispdb']['smtp']))
			{
				$content['smtp_output'] .= lang('Using data from Mozilla ISPDB for provider %1', $content['ispdb']['displayName'])."\n";
				$hosts = array();
				foreach($content['ispdb']['smtp'] as $server)
				{
					if (!isset($hosts[$server['hostname']]))
					{
						$hosts[$server['hostname']] = array('username' => $server['username']);
					}
					if (strtoupper($server['socketType']) == 'SSL')	// try TLS first
					{
						$hosts[$server['hostname']]['TLS'] = $server['port'];
					}
					$hosts[$server['hostname']][strtoupper($server['socketType'])] = $server['port'];
					// make sure we prefer SSL over STARTTLS over insecure
					if (count($hosts[$server['hostname']]) > 2)
					{
						$hosts[$server['hostname']] = self::fix_ssl_order($hosts[$server['hostname']]);
					}
				}
			}
			else
			{
				$hosts = $this->guess_hosts($content['ident_email'], 'smtp');
			}
			foreach($hosts as $host => $data)
			{
				$content['acc_smtp_host'] = $host;
				if (!is_array($data))
				{
					$data = array('TLS' => 465, 'SSL' => 465, 'STARTTLS' => 587, '' => 25);
				}
				foreach($data as $ssl => $port)
				{
					if ($ssl === 'username') continue;

					$content['acc_smtp_ssl'] = (int)self::$ssl2type[$ssl];

					$e = null;
					try {
						$content['smtp_output'] .= "\n".Api\DateTime::to('now', 'H:i:s').": Trying $ssl connection to $host:$port ...\n";
						$content['acc_smtp_port'] = $port;

						$params = [
							'username' => $content['acc_smtp_username'],
							'password' => $content['acc_smtp_password'],
							'host' => $content['acc_smtp_host'],
							'port' => $content['acc_smtp_port'],
							'secure' => self::$ssl2secure[(string)array_search($content['acc_smtp_ssl'], self::$ssl2type)],
							'timeout' => self::TIMEOUT,
							'debug' => self::DEBUG_LOG,
						];
						if (!empty($content['acc_oauth_provider_url']))
						{
							$params['xoauth2_token'] = self::oauthToken($content, true);
						}
						$mail = new Horde_Mail_Transport_Smtphorde($params);
						// create smtp connection and authenticate, if credentials given
						$smtp = $mail->getSMTPObject();
						$content['smtp_output'] .= "\n".lang('Successful connected to %1 server%2.', 'SMTP',
							(!empty($content['acc_smtp_username']) ? ' '.lang('and logged in') : ''))."\n";
						if (!$smtp->isSecureConnection())
						{
							if (!empty($content['acc_smtp_username']))
							{
								$content['smtp_output'] .= lang('Connection is NOT secure! Everyone can read eg. your credentials.')."\n";
							}
							$content['acc_smtp_ssl'] = 'no';
						}
						// Horde_Smtp always try to use STARTTLS, adjust our ssl-parameter if successful
						elseif (!($content['acc_smtp_ssl'] > self::SSL_NONE))
						{
							//error_log(__METHOD__."() new Horde_Mail_Transport_Smtphorde(".array2string($params).")->getSMTPObject()->isSecureConnection()=".array2string($smtp->isSecureConnection()));
							$content['acc_smtp_ssl'] = self::SSL_STARTTLS;
						}
						// try sending a mail to a different domain, if not authenticated, to see if that's required
						if (empty($content['acc_smtp_username']))
						{
							$smtp->send($content['ident_email'], 'noreply@example.com', '');
							$content['smtp_output'] .= "\n".lang('Relay access checked')."\n";
						}
						$content['smtp_connected'] = true;
						unset($content['button']);
						return $this->edit($content, lang('Successful connected to %1 server%2.', 'SMTP',
							empty($content['acc_smtp_username']) ? ' - '.lang('Relay access checked') : ' '.lang('and logged in')));
					}
					// unfortunately LOGIN_AUTHENTICATIONFAILED and SERVER_CONNECT are thrown as Horde_Mail_Exception
					// while others are thrown as Horde_Smtp_Exception --> using common base Horde_Exception_Wrapped
					catch(Horde_Exception_Wrapped $e)
					{
						switch($e->getCode())
						{
							case Horde_Smtp_Exception::LOGIN_AUTHENTICATIONFAILED:
							case Horde_Smtp_Exception::LOGIN_REQUIREAUTHENTICATION:
							case Horde_Smtp_Exception::UNSPECIFIED:
								$content['smtp_output'] .= "\n".$e->getMessage()."\n";
								break;
							case Horde_Smtp_Exception::SERVER_CONNECT:
								$content['smtp_output'] .= "\n".$e->getMessage()."\n";
								break;
							default:
								$content['smtp_output'] .= "\n".$e->getMessage().' ('.$e->getCode().')'."\n";
								break;
						}
						if (self::$debug) _egw_log_exception($e);
					}
					catch(Horde_Smtp_Exception $e)
					{
						// prever $e->details over $e->getMessage() as it contains original message from SMTP server (eg. relay access denied)
						$content['smtp_output'] .= "\n".(empty($e->details) ? $e->getMessage().' ('.$e->getCode().')' : $e->details)."\n";
						//$content['smtp_output'] .= $e->getTraceAsString()."\n";
						if (self::$debug) _egw_log_exception($e);
					}
					catch(Exception $e) {
						$content['smtp_output'] .= "\n".get_class($e).': '.$e->getMessage().' ('.$e->getCode().')'."\n";
						//$content['smtp_output'] .= $e->getTraceAsString()."\n";
						if (self::$debug) _egw_log_exception($e);
					}
				}
			}
		}
		// add validation error, if we can identify a field
		if (!$content['smtp_connected'] && $e instanceof Horde_Exception_Wrapped)
		{
			switch($e->getCode())
			{
				case Horde_Smtp_Exception::LOGIN_AUTHENTICATIONFAILED:
				case Horde_Smtp_Exception::LOGIN_REQUIREAUTHENTICATION:
				case Horde_Smtp_Exception::UNSPECIFIED:
					Etemplate::set_validation_error('acc_smtp_username', lang($e->getMessage()));
					Etemplate::set_validation_error('acc_smtp_password', lang($e->getMessage()));
					break;

				case Horde_Smtp_Exception::SERVER_CONNECT:
					Etemplate::set_validation_error('acc_smtp_host', lang($e->getMessage()));
					Etemplate::set_validation_error('acc_smtp_port', lang($e->getMessage()));
					break;
			}
		}
		$sel_options['acc_smtp_ssl'] = self::$ssl_types;
		$tpl = new Etemplate('admin.mailwizard.smtp');
		$tpl->exec(static::APP_CLASS.'smtp', $content, $sel_options, $readonlys, $content, 2);
	}

	/**
	 * Edit mail account(s)
	 *
	 * Gets either called with GET parameter:
	 *
	 * a) account_id from admin >> Manage users to edit / add mail accounts for a user
	 *    --> shows selectbox to switch between different mail accounts of user and "create new account"
	 *
	 * b) via mail_wizard proxy class by regular mail user to edit (acc_id GET parameter) or create new mail account
	 *
	 * @param ?array $content =null
	 * @param string $msg =''
	 * @param string $msg_type ='success'
	 */
	public function edit(?array $content=null, $msg='', $msg_type='success')
	{
		// app is trying to tell something, while redirecting to wizard
		if (empty($content) && $_GET['acc_id'] && empty($msg) && !empty( $_GET['msg']))
		{
			if (stripos($_GET['msg'],'fatal error:')!==false || $_GET['msg_type'] == 'error') $msg_type = 'error';
		}
		if ($content['acc_id'] || (isset($_GET['acc_id']) && (int)$_GET['acc_id'] > 0) ) Mail::unsetCachedObjects($content['acc_id']?$content['acc_id']:$_GET['acc_id']);
		$tpl = new Etemplate('admin.mailaccount');

		if (!is_array($content) || !empty($content['acc_id']) && isset($content['old_acc_id']) && $content['acc_id'] != $content['old_acc_id'])
		{
			if (!is_array($content)) $content = array();
			if ($this->is_admin && isset($_GET['account_id']))
			{
				$content['called_for'] = (int)$_GET['account_id'];
				$content['accounts'] = iterator_to_array(Mail\Account::search($content['called_for']));
				if (!empty($content['accounts']))
				{
					$content['acc_id'] = key($content['accounts']);
					//error_log(__METHOD__.__LINE__.'.'.array2string($content['acc_id']));
					// test if the "to be selected" account is imap or not
					if (is_array($content['accounts']) && count($content['accounts'])>1 && Mail\Account::is_multiple($content['acc_id']))
					{
						try {
							$account = Mail\Account::read($content['acc_id'], $content['called_for']);
							//try to select the first account that is of type imap
							if (!$account->is_imap())
							{
								$content['acc_id'] = key($content['accounts']);
								//error_log(__METHOD__.__LINE__.'.'.array2string($content['acc_id']));
							}
						}
						catch(Api\Exception\NotFound $e) {
							if (self::$debug) _egw_log_exception($e);
						}
					}
				}
				if (!$content['accounts'])	// no email account, call wizard
				{
					return $this->add(array('account_id' => (int)$_GET['account_id']));
				}
				$content['accounts']['new'] = lang('Create new account');
			}
			if (isset($_GET['acc_id']) && (int)$_GET['acc_id'] > 0)
			{
				$content['acc_id'] = (int)$_GET['acc_id'];
			}
			// clear current account-data, as account has changed and we going to read selected one
			$content = array_intersect_key($content, array_flip(array('called_for', 'accounts', 'acc_id', 'tabs')));

			if ($content['acc_id'] === 'new')
			{
				$content['account_id'] = $content['called_for'];
				$content['old_acc_id'] = $content['acc_id'];	// to not call add/wizard, if we return from to
				unset($content['tabs']);
				return $this->add($content);
			}
			elseif ($content['acc_id'] > 0)
			{
				try {
					$account = Mail\Account::read($content['acc_id'], $this->is_admin && !empty($content['called_for']) ?
						$content['called_for'] : $GLOBALS['egw_info']['user']['account_id']);
					$account->getUserData();	// quota, aliases, forwards etc.
					$content += $account->params;
					foreach(['acc_imap_password', 'acc_smtp_password'] as $n)
					{
						if (isset($content['acc_oauth_username']) && $content[$n] === Mail\Credentials::UNAVAILABLE)
						{
							unset($content[$n]);
						}
					}
					$content['notify_use_default'] = !$content['notify_account_id'];
					self::fix_account_id_0($content['account_id']);

					// read identities (of current user) and mark std identity
					$content['identities'] = iterator_to_array(Mail\Account::identities($account, true, 'name', $content['called_for']));
					$content['std_ident_id'] = $content['ident_id'];
					$content['identities'][$content['std_ident_id']] = lang('Standard identity');
					// change self::SSL_NONE (=0) to "no" used in sel_options
					foreach(array('imap','smtp','sieve') as $type)
					{
						if (!$content['acc_'.$type.'_ssl']) $content['acc_'.$type.'_ssl'] = 'no';
					}
				}
				catch(Api\Exception\NotFound $e) {
					if (self::$debug) _egw_log_exception($e);
					Framework::window_close(lang('Account not found!'));
				}
				catch(Exception $e) {
					if (self::$debug) _egw_log_exception($e);
					Framework::window_close($e->getMessage().' ('.get_class($e).': '.$e->getCode().')');
				}
			}
		}
		// some defaults for new accounts
		if (!isset($content['account_id']) || empty($content['acc_id']) || $content['acc_id'] === 'new')
		{
			if (!isset($content['account_id'])) $content['account_id'] = array($GLOBALS['egw_info']['user']['account_id']);
			$content['acc_user_editable'] = $content['acc_further_identities'] = true;
			$readonlys['ident_id'] = true;	// need to create standard identity first
		}
		if (empty($content['acc_name']))
		{
			$content['acc_name'] = $content['ident_email'];
		}
		// disable some stuff for non-emailadmins (all values are preserved!)
		if (!$this->is_admin)
		{
			$readonlys = array(
				'account_id' => true, 'button[multiple]' => true, 'acc_user_editable' => true,
				'acc_further_identities' => true,
				'acc_imap_type' => true, 'acc_imap_logintype' => true, 'acc_domain' => true,
				'acc_imap_admin_username' => true, 'acc_imap_admin_password' => true, 'acc_imap_admin_use_without_pw' => true,
				'acc_smtp_type' => true, 'acc_smtp_auth_session' => true,
			);
		}
		// ensure correct values for single user mail accounts (we only hide them client-side)
		if (!($is_multiple = Mail\Account::is_multiple($content)))
		{
			$content['acc_imap_type'] = 'EGroupware\\Api\\Mail\\Imap';
			unset($content['acc_imap_login_type']);
			$content['acc_smtp_type'] = 'EGroupware\\Api\\Mail\\Smtp';
			unset($content['acc_smtp_auth_session']);
			unset($content['notify_use_default']);
		}
		// copy ident_email_alias selectbox back to regular name
		elseif (isset($content['ident_email_alias']) && !empty ($content['ident_email_alias']))
		{
			$content['ident_email'] = $content['ident_email_alias'];
		}
		$edit_access = Mail\Account::check_access(Acl::EDIT, $content);

		// disable notification save-default and use-default, if only one account or no edit-rights
		$tpl->disableElement('notify_save_default', !$is_multiple || !$edit_access);
		$tpl->disableElement('notify_use_default', !$is_multiple);

		if (!empty($content['button']))
		{
			$button = key($content['button']);
			unset($content['button']);
			switch($button)
			{
				case 'wizard':
					// if we just came from wizard, go back to last page/step
					if (isset($content['smtp_connected']))
					{
						return $this->smtp($content);
					}
					// otherwise start with first step
					return $this->autoconfig($content);

				case 'delete_identity':
					// delete none-standard identity of current user
					if (($this->is_admin || $content['acc_further_identities']) &&
						$content['ident_id'] > 0 && $content['std_ident_id'] != $content['ident_id'])
					{
						Mail\Account::delete_identity($content['ident_id']);
						$msg = lang('Identity deleted');
						unset($content['identities'][$content['ident_id']]);
						$content['ident_id'] = $content['std_ident_id'];
					}
					break;

				case 'save':
				case 'apply':
					try {
						// save none-standard identity for current user
						if ($content['acc_id'] && $content['acc_id'] !== 'new' &&
							($this->is_admin || $content['acc_further_identities']) &&
							$content['std_ident_id'] != $content['ident_id'])
						{
							$content['ident_id'] = Mail\Account::save_identity(array(
								'account_id' => $content['called_for'] ? $content['called_for'] : $GLOBALS['egw_info']['user']['account_id'],
							)+$content);
							$content['identities'][$content['ident_id']] = Mail\Account::identity_name($content);
							$msg = lang('Identity saved.');
							if ($edit_access) $msg .= ' '.lang('Switch back to standard identity to save account.');
						}
						elseif ($edit_access)
						{
							// if admin username/password given, check if it is valid
							$account = new Mail\Account($content);
							if ($account->acc_imap_administration)
							{
								try {
									$imap = $account->imapServer(true);
									if ($imap) $imap->checkAdminConnection();
								}
								catch(\Horde_Imap_Client_Exception $e) {
									Api\Json\Response::get()->message(lang('Checking admin credentials failed').': '.$e->getMessage(), 'info');
								}
							}
							// test sieve connection, if not called for other user, enabled and credentials available
							if (!$content['called_for'] && $account->acc_sieve_enabled && $account->acc_imap_username)
							{
								$account->imapServer()->retrieveRules();
							}
							$new_account = !((int)$content['acc_id'] > 0);
							// check for deliveryMode="forwardOnly", if a forwarding-address is given
							if ($content['acc_smtp_type'] != 'EGroupware\\Api\\Mail\\Smtp' &&
								$content['deliveryMode'] == Mail\Smtp::FORWARD_ONLY &&
								empty($content['mailForwardingAddress']))
							{
								Etemplate::set_validation_error('mailForwardingAddress', lang('Field must not be empty !!!'));
								throw new Api\Exception\WrongUserinput(lang('You need to specify a forwarding address, when checking "%1"!', lang('Forward only')));
							}
							// set notifications to store according to checkboxes
							if ($content['notify_save_default'])
							{
								$content['notify_account_id'] = 0;
							}
							elseif (!$content['notify_use_default'])
							{
								$content['notify_account_id'] = $content['called_for'] ?
									$content['called_for'] : $GLOBALS['egw_info']['user']['account_id'];
							}
							// SMIME SAVE
							if (isset($content['smimeKeyUpload']))
							{
								$content['acc_smime_cred_id'] = self::save_smime_key($content, $tpl, $content['called_for']);
								unset($content['smimeKeyUpload']);
							}
							self::fix_account_id_0($content['account_id'], true);
							$content = Mail\Account::write($content, !empty($content['called_for']) && $this->is_admin ?
								$content['called_for'] : $GLOBALS['egw_info']['user']['account_id']);
							self::fix_account_id_0($content['account_id']);
							$msg = lang('Account saved.');
							// user wants default notifications
							if ($content['acc_id'] && $content['notify_use_default'])
							{
								// delete own ones
								Mail\Notifications::delete($content['acc_id'], $content['called_for'] ?
									$content['called_for'] : $GLOBALS['egw_info']['user']['account_id']);
								// load default ones
								$content = array_merge($content, Mail\Notifications::read($content['acc_id'], 0));
							}
							// add new std identity entry
							if ($new_account)
							{
								$content['std_ident_id'] = $content['ident_id'];
								$content['identities'] = array(
									$content['std_ident_id'] => lang('Standard identity'));
							}
							if (isset($content['accounts']))
							{
								if (!isset($content['accounts'][$content['acc_id']]))	// insert new account as top, not bottom
								{
									$content['accounts'] = array($content['acc_id'] => '') + $content['accounts'];
								}
								$content['accounts'][$content['acc_id']] = Mail\Account::identity_name($content, false);
							}
						}
						else
						{
							if ($content['notify_use_default'] && $content['notify_account_id'])
							{
								// delete own ones
								if (Mail\Notifications::delete($content['acc_id'], $content['called_for'] ?
									$content['called_for'] : $GLOBALS['egw_info']['user']['account_id']))
								{
									$msg = lang('Notification folders updated.');
								}
								// load default ones
								$content = array_merge($content, Mail\Notifications::read($content['acc_id'], 0));
							}
							if (!$content['notify_use_default'] && is_array($content['notify_folders']))
							{
								$content['notify_account_id'] = $content['called_for'] ?
									$content['called_for'] : $GLOBALS['egw_info']['user']['account_id'];
								if (Mail\Notifications::write($content['acc_id'], $content['notify_account_id'],
									$content['notify_folders']))
								{
									$msg = lang('Notification folders updated.');
								}
							}
							if ($content['acc_user_forward'] && !empty($content['acc_smtp_type']) && $content['acc_smtp_type'] != 'EGroupware\\Api\\Mail\\Smtp')
							{
								$account = new Mail\Account($content);
								$account->smtpServer()->saveSMTPForwarding($content['called_for'] ?
									$content['called_for'] : $GLOBALS['egw_info']['user']['account_id'],
									$content['mailForwardingAddress'],
									$content['forwardOnly'] ? null : 'yes');
							}
							// smime (private) key uploaded by user himself
							if (!empty($content['smimeKeyUpload']))
							{
								$content['acc_smime_cred_id'] = self::save_smime_key($content, $tpl);
								unset($content['smimeKeyUpload']);
							}
						}
					}
					catch (Horde_Imap_Client_Exception $e)
					{
						_egw_log_exception($e);
						$tpl->set_validation_error('acc_imap_admin_username', $msg=lang($e->getMessage()).($e->details?', '.lang($e->details):''));
						$msg_type = 'error';
						$content['tabs'] = 'admin.mailaccount.imap';	// should happen automatic
						break;
					}
					catch (Horde\ManageSieve\Exception\ConnectionFailed $e)
					{
						_egw_log_exception($e);
						$tpl->set_validation_error('acc_sieve_port', $msg=lang($e->getMessage()));
						$msg_type = 'error';
						$content['tabs'] = 'admin.mailaccount.sieve';	// should happen automatic
						break;
					}
					catch (Exception $e) {
						$msg = lang('Error saving account!')."\n".$e->getMessage();
						$button = 'apply';
						$msg_type = 'error';
					}
					if ($content['acc_id']) Mail::unsetCachedObjects($content['acc_id']);
					if (stripos($msg,'fatal error:')!==false) $msg_type = 'error';
					Framework::refresh_opener($msg, 'mail-account', $content['acc_id'], $new_account ? 'add' : 'update', null, null, null, $msg_type);
					if ($button == 'save') Framework::window_close();
					break;

				case 'delete':
					if (!Mail\Account::check_access(Acl::DELETE, $content))
					{
						$msg = lang('Permission denied!');
						$msg_type = 'error';
					}
					elseif (Mail\Account::delete($content['acc_id']) > 0)
					{
						if ($content['acc_id']) Mail::unsetCachedObjects($content['acc_id']);
						Framework::refresh_opener(lang('Account deleted.'), 'mail-account', $content['acc_id'], 'delete');
						Framework::window_close();
					}
					else
					{
						$msg = lang('Failed to delete account!');
						$msg_type = 'error';
					}
			}
		}
		// SMIME UPLOAD/DELETE/EXPORT control
		$content['hide_smime_upload'] = false;
		if (!empty($content['acc_smime_cred_id']))
		{
			if (!empty($content['smime_delete_p12']) &&
					Mail\Credentials::delete (
						$content['acc_id'],
						$content['called_for'] ? $content['called_for'] : $GLOBALS['egw_info']['user']['account_id'],
						Mail\Credentials::SMIME
				))
			{
				unset($content['acc_smime_password'], $content['smimeKeyUpload'], $content['smime_delete_p12'], $content['acc_smime_cred_id']);
				$content['hide_smime_upload'] = false;
			}
			else
			{
				// do NOT send smime private key to client side, it's unnecessary and binary blob breaks json encoding
				$content['acc_smime_password'] = Mail\Credentials::UNAVAILABLE;

				$content['hide_smime_upload'] = true;
			}
		}

		// disable delete button for new, not yet saved entries, if no delete rights or a non-standard identity selected
		$readonlys['button[delete]'] = empty($content['acc_id']) ||
			!Mail\Account::check_access(Acl::DELETE, $content) ||
			$content['ident_id'] != $content['std_ident_id'];

		// if account is for multiple user, change delete confirmation to reflect that
		$tpl->setElementAttribute('button[delete]', 'onclick', !Mail\Account::is_multiple($content) ?
			"Et2Dialog.confirm(widget,'Delete this account','Delete')" :
			"Et2Dialog.confirm(widget,'This is NOT a personal mail account!\\n\\nAccount will be deleted for ALL users!\\n\\nAre you really sure you want to do that?','Delete this account')");

		// if no edit access, make whole dialog readonly
		if (!$edit_access)
		{
			$readonlys['__ALL__'] = true;
			$readonlys['button[cancel]'] = false;
			// allow to edit notification-folders
			$readonlys['button[save]'] = $readonlys['button[apply]'] =
			$readonlys['notify_folders'] = $readonlys['notify_use_default'] = false;
			// allow to edit sMime stuff
			$readonlys['smimeGenerate'] = $readonlys['smimeKeyUpload'] = $readonlys['smime_pkcs12_password'] =
			$readonlys['smime_export_p12'] = $readonlys['smime_delete_p12'] = false;
		}

		$sel_options['acc_imap_ssl'] = $sel_options['acc_sieve_ssl'] =
			$sel_options['acc_smtp_ssl'] = self::$ssl_types;

		// admin access to account with no credentials available
		if ($this->is_admin && (!empty($content['called_for']) || empty($content['acc_imap_host']) || $content['called_for']) ||
			// if OAuth failed, do not try to connect and trigger next authentication(-failure), but show failure message
			!empty($content['oauth_failure']))
		{
			// can't connection to imap --> allow free entries in taglists
			foreach(array('acc_folder_sent', 'acc_folder_trash', 'acc_folder_draft', 'acc_folder_template', 'acc_folder_junk') as $folder)
			{
				$tpl->setElementAttribute($folder, 'allowFreeEntries', true);
			}
		}
		else
		{
			try {
				if (($oauth = OpenIDConnectClient::providerByDomain(
					$content['acc_oauth_username'] ?? $content['acc_imap_username'] ?? $content['ident_email'], $content['acc_imap_host'])))
				{
					$content += self::oauth2content($oauth);
				}
				$sel_options['acc_folder_sent'] = $sel_options['acc_folder_trash'] =
					$sel_options['acc_folder_draft'] = $sel_options['acc_folder_template'] =
					$sel_options['acc_folder_junk'] = $sel_options['acc_folder_archive'] =
					$sel_options['notify_folders'] = $sel_options['acc_folder_ham'] =
						self::mailboxes(self::imap_client ($content));
				// Allow folder notification on INBOX for popup_only
				if ($GLOBALS['egw_info']['user']['preferences']['notifications']['notification_chain'] == 'popup_only')
				{
					$sel_options['notify_folders']['INBOX'] = lang('INBOX');
				}
			}
			catch(Exception $e) {
				if (self::$debug) _egw_log_exception($e);
				// let user know what the problem is and that he can fix it using wizard or deleting
				$msg = lang($e->getMessage())."\n\n".lang('You can use wizard to fix account settings or delete account.');
				$msg_type = 'error';
				// cant connection to imap --> allow free entries in taglists
				foreach(array('acc_folder_sent', 'acc_folder_trash', 'acc_folder_draft', 'acc_folder_template', 'acc_folder_junk') as $folder)
				{
					$tpl->setElementAttribute($folder, 'allowFreeEntries', true);
				}
			}
		}

		$sel_options['acc_imap_type'] = Mail\Types::getIMAPServerTypes(false);
		$sel_options['acc_smtp_type'] = Mail\Types::getSMTPServerTypes(false);
		$sel_options['acc_imap_logintype'] = self::$login_types;
		$sel_options['ident_id'] = $content['identities'];
		$sel_options['acc_id'] = $content['accounts'];
		$sel_options['acc_further_identities'] = self::$further_identities;

		// user is allowed to create or edit further identities
		if ($edit_access || $content['acc_further_identities'])
		{
			$sel_options['ident_id']['new'] = lang('Create new identity');
			$readonlys['ident_id'] = false;

			// if no edit-access and identity is not standard identity --> allow to edit identity
			if (!$edit_access && $content['ident_id'] != $content['std_ident_id'])
			{
				$readonlys += array(
					'button[save]' => false, 'button[apply]' => false,
					'button[placeholders]' => false,
					'ident_name' => false,
					'ident_realname' => false, 'ident_email' => false, 'ident_email_alias' => false,
					'ident_org' => false, 'ident_signature' => false,
				);
			}
			if ($content['ident_id'] != $content['old_ident_id'] &&
				($content['old_ident_id'] || $content['ident_id'] != $content['std_ident_id']))
			{
				if ((int)$content['ident_id'] > 0)
				{
					$identity = Mail\Account::read_identity($content['ident_id'], false, $content['called_for']);
					unset($identity['account_id']);
					$content = array_merge($content, $identity, array('ident_email_alias' => $identity['ident_email']));
				}
				else
				{
					$content['ident_name'] = $content['ident_realname'] = $content['ident_email'] =
						$content['ident_email_alias'] = $content['ident_org'] = $content['ident_signature'] = '';
				}
				if (empty($msg) && $edit_access && $content['ident_id'] && $content['ident_id'] != $content['std_ident_id'])
				{
					$msg = lang('Switch back to standard identity to save other account data.');
					$msg_type = 'help';
				}
				$content['old_ident_id'] = $content['ident_id'];
			}
		}
		$content['old_acc_id'] = $content['acc_id'];

		// if only aliases are allowed for futher identities, add them as options
		// allow admins to always add arbitrary aliases
		if ($content['acc_further_identities'] == 2 && !$this->is_admin)
		{
			$sel_options['ident_email_alias'] = array_merge(
				array('' => $content['mailLocalAddress'].' ('.lang('Default').')'),
				array_combine($content['mailAlternateAddress'] ?? [], $content['mailAlternateAddress'] ?? []));
			// if admin explicitly set a non-alias, we need to add it to aliases to keep it after storing signature by user
			if ($content['ident_email'] !== $content['mailLocalAddress'] && !isset($sel_options['ident_email_alias'][$content['ident_email']]))
			{
				$sel_options['ident_email_alias'][$content['ident_email']] = $content['ident_email'];
			}
			// copy ident_email to select-box ident_email_alias, as et2 requires unique ids
			$content['ident_email_alias'] = $content['ident_email'];
			$content['select_ident_mail'] = true;
		}

		// only allow to delete further identities, not a standard identity
		$readonlys['button[delete_identity]'] = !($content['ident_id'] > 0 && $content['ident_id'] != $content['std_ident_id']);

		// disable aliases tab for default smtp class EGroupware\Api\Mail\Smtp
		$readonlys['tabs']['admin.mailaccount.aliases'] = !$content['acc_smtp_type'] ||
			$content['acc_smtp_type'] == 'EGroupware\\Api\\Mail\\Smtp';
		if ($readonlys['tabs']['admin.mailaccount.aliases'])
		{
			unset($sel_options['acc_further_identities'][2]);	// can limit identities to aliases without aliases ;-)
		}

		// allow smtp class to disable certain features in alias tab
		if ($content['acc_smtp_type'] && class_exists($content['acc_smtp_type']) &&
			is_a($content['acc_smtp_type'], 'EGroupware\\Api\\Mail\\Smtp\\Ldap', true))
		{
			$content['no_forward_available'] = !constant($content['acc_smtp_type'].'::FORWARD_ATTR');
			if (!constant($content['acc_smtp_type'].'::FORWARD_ONLY_ATTR'))
			{
				$readonlys['deliveryMode'] = true;
			}
		}

		// account allows users to change forwards
		if (!$edit_access && !$readonlys['tabs']['admin.mailaccount.aliases'] && $content['acc_user_forward'])
		{
			$readonlys['mailForwardingAddress'] = false;
		}

		// allow imap classes to disable certain tabs or fields
		if (($class = Mail\Account::getIcClass($content['acc_imap_type'])) && class_exists($class) &&
			($imap_ro = call_user_func(array($class, 'getUIreadonlys'))))
		{
			$readonlys = array_merge($readonlys, $imap_ro, array(
				'tabs' => array_merge((array)$readonlys['tabs'], (array)$imap_ro['tabs']),
			));
		}
		Framework::message($msg ? $msg : (string)$_GET['msg'], $msg_type);

		// when called by admin for existing accounts, display further administrative actions
		if ($content['called_for'] && (int)$content['acc_id'] > 0)
		{
			$admin_actions = array();
			foreach(Api\Hooks::process(array(
				'location' => 'emailadmin_edit',
				'account_id' => $content['called_for'],
				'acc_id' => $content['acc_id'],
			)) as $actions)
			{
				if ($actions) $admin_actions = array_merge($admin_actions, $actions);
			}
			if ($admin_actions) $tpl->setElementAttribute('admin_actions', 'actions', $admin_actions);
		}
		$content['admin_actions'] = (bool)$admin_actions;

		//try to fix identities with no domain part set e.g. alias as identity
		if (!strpos($content['ident_email'], '@'))
		{
			$content['ident_email'] = Mail::fixInvalidAliasAddress (Api\Accounts::id2name($content['acc_imap_account_id'], 'account_email'), $content['ident_email']);
		}

		// If no EPL available, show that in spamtitan blur
		$content['spamtitan_blur'] = $GLOBALS['egw_info']['user']['apps']['stylite'] ? '' : lang('SpamTitan integration requires EPL version');

		$tpl->exec(static::APP_CLASS.'edit', $content, $sel_options, $readonlys, $content, 2);
	}

	/**
	 * Saves the smime key
	 *
	 * @param array $content
	 * @param Etemplate $tpl
	 * @param int $account_id =null account to save smime key for, default current user
	 * @return int cred_id or null on error
	 */
	private static function save_smime_key(array $content, Etemplate $tpl, $account_id=null)
	{
		if (($pkcs12 = file_get_contents($content['smimeKeyUpload']['tmp_name'])))
		{
			$cert_info = Mail\Smime::extractCertPKCS12($pkcs12, $content['smime_pkcs12_password']);
			if (is_array($cert_info) && !empty($cert_info['cert']))
			{
				// save public key
				$smime = new Mail\Smime;
				$email = $smime->getEmailFromKey($cert_info['cert']);
				$AB_bo = new addressbook_bo();
				$AB_bo->set_smime_keys(array(
					$email => $cert_info['cert']
				));
				// save private key
				if (!isset($account_id)) $account_id = $GLOBALS['egw_info']['user']['account_id'];
				return Mail\Credentials::write($content['acc_id'], $email, $pkcs12, Mail\Credentials::SMIME, $account_id);
			}
			$tpl->set_validation_error('smimeKeyUpload', lang('Could not extract private key from given p12 file. Either the p12 file is broken or password is wrong!'));
		}
		return null;
	}

	/**
	 * Replace 0 with '' or back
	 *
	 * @param string|array &$account_id on return always array
	 * @param boolean $back =false
	 */
	private static function fix_account_id_0(&$account_id=null, $back=false)
	{
		if (!isset($account_id)) return;

		if (!is_array($account_id))
		{
			$account_id = $account_id ? explode(',', $account_id) : [];
		}
		if ($back && !$account_id)
		{
			$account_id = 0;
		}
		if (!$back && count($account_id) === 1 && !current($account_id))
		{
			$account_id = [];
		}
	}

	/**
	 * Instanciate imap-client
	 *
	 * @param array $content
	 * @param int $timeout =null default use value returned by Mail\Imap::getTimeOut()
	 * @return Horde_Imap_Client_Socket
	 */
	protected static function imap_client(array &$content, $timeout=null)
	{
		$config = [
			'username' => $content['acc_imap_username'],
			'password' => $content['acc_imap_password'],
			'hostspec' => $content['acc_imap_host'],
			'port' => $content['acc_imap_port'],
			'secure' => self::$ssl2secure[(string)array_search($content['acc_imap_ssl'], self::$ssl2type)],
			'timeout' => $timeout > 0 ? $timeout : Mail\Imap::getTimeOut(),
			'debug' => self::DEBUG_LOG,
		];
		if (!empty($content['acc_oauth_provider_url']) || !empty($content['acc_oauth_access_token']))
		{
			$config['xoauth2_token'] = self::oauthToken($content);
			$config['username'] = $content['acc_oauth_username'] ?? $content['acc_imap_username'];
			if (empty($config['password'])) $config['password'] = '**oauth**';    // some password is required, even if not used
		}
		return new Horde_Imap_Client_Socket($config);
	}

	/**
	 * Acquire OAuth access (and refresh) token
	 */
	protected static function oauthToken(array &$content, bool $smtp=false)
	{
		if (empty($content['acc_oauth_access_token']))
		{
			if (empty($content['acc_oauth_client_secret']) &&
				($oauth = OpenIDConnectClient::providerByDomain($content['acc_oauth_username'] ?? $content['acc_imap_username'] ?? $content['ident_email'], $content['acc_imap_host'])))
			{
				$content += self::oauth2content($oauth);
			}
			if (empty($content['acc_oauth_client_secret']))
			{
				throw new Exception(lang("No OAuth client secret for provider '%1'!", $content['acc_oauth_provider_url']));
			}
			$oidc = new OpenIDConnectClient($content['acc_oauth_provider_url'],
				$content['acc_oauth_client_id'], $content['acc_oauth_client_secret']);

			// Office365 requires client-ID as appid GET parameter (https://github.com/jumbojett/OpenID-Connect-PHP/issues/190)
			if (!empty($content[OpenIDConnectClient::ADD_CLIENT_TO_WELL_KNOWN]))
			{
				$oidc->setWellKnownConfigParameters([$content[OpenIDConnectClient::ADD_CLIENT_TO_WELL_KNOWN] => $content['acc_oauth_client_id']]);
			}
			// Google requires access_type=offline&prompt=consent to return a refresh-token
			if (!empty($content[OpenIDConnectClient::ADD_AUTH_PARAM]))
			{
				$oidc->addAuthParam(str_replace('$username', $content['acc_oauth_username'] ?? $content['acc_imap_username'] ?? $content['ident_email'], $content[OpenIDConnectClient::ADD_AUTH_PARAM]));
			}

			// we need to use response_code=query / GET request to keep our session token!
			$oidc->setResponseTypes(['code']);  // to be able to use query, not 'id_token'
			//$oidc->setAllowImplicitFlow(true);
			$oidc->addScope($content['acc_oauth_scopes']);
		}

		if (!empty($content['acc_oauth_access_token']) ||
			!empty($content['acc_oauth_refresh_token']) && $content['acc_oauth_refresh_token'] !== Mail\Credentials::UNAVAILABLE)
		{
			if (empty($content['acc_oauth_access_token']))
			{
				$content['acc_oauth_access_token'] = $oidc->refreshToken($content['acc_oauth_refresh_token'])->access_token;
			}
			if (!empty($content['acc_oauth_access_token']))
			{
				if ($smtp)
				{
					return new Horde_Smtp_Password_Xoauth2($content['acc_oauth_username'] ?? $content['acc_smtp_username'], $content['acc_oauth_access_token']);
				}
				return new Horde_Imap_Client_Password_Xoauth2($content['acc_oauth_username'] ?? $content['acc_imap_username'], $content['acc_oauth_access_token']);
			}
		}
		// Run OAuth authentication, will NOT return, but call success or failure callbacks below
		$oidc->authenticateThen(__CLASS__.'::oauthAuthenticated', [$content], __CLASS__.'::oauthFailure', [$content]);
	}

	/**
	 * Oauth success callback calling autoconfig again
	 *
	 * @param OpenIDConnectClient $oidc
	 * @param array $content
	 * @return void
	 */
	public static function oauthAuthenticated(OpenIDConnectClient $oidc, array $content)
	{
		if (empty($content['acc_oauth_username']))
		{
			$content['acc_oauth_username'] = $content['acc_imap_username'] ?? $oidc->getVerifiedClaims('email') ?? $content['ident_email'];
		}
		if (empty($content['acc_oauth_refresh_token'] = $oidc->getRefreshToken()))
		{
			$content['output'] .= lang('OAuth Authentication').': '.lang('Successful, but NO refresh-token received!');
			$content['connected'] = false;
		}
		$content['acc_oauth_access_token'] = $oidc->getAccessToken();

		if (empty($GLOBALS['egw_info']['user']['apps']['admin']))
		{
			$GLOBALS['egw_info']['flags']['currentapp'] = 'mail';
			$obj = new mail_wizard();
		}
		else
		{
			$GLOBALS['egw_info']['flags']['currentapp'] = 'admin';
			$obj = new self;
		}
		unset($content['oauth_failure']);
		if (!empty($content['acc_id']))
		{
			$content['button'] = ['save' => true];  // automatic save token, refresh mail app and close popup
			$obj->edit($content, lang('Use save or apply to store the received OAuth token!'), 'info');
		}
		else
		{
			$obj->autoconfig($content);
		}
	}

	/**
	 * Oauth failure callback calling autoconfig again
	 *
	 * @param OpenIDConnectClientException|null $exception
	 * @param array $content
	 */
	public static function oauthFailure(Throwable  $exception=null, array $content)
	{
		if (empty($GLOBALS['egw_info']['user']['apps']['admin']))
		{
			$GLOBALS['egw_info']['flags']['currentapp'] = 'mail';
			$obj = new mail_wizard();
		}
		else
		{
			$GLOBALS['egw_info']['flags']['currentapp'] = 'admin';
			$obj = new self;
		}
		$content['oauth_failure'] = $exception ?: true;
		if (!empty($content['acc_id']))
		{
			$obj->edit($content, lang('OAuth Authentiction').': '.($exception ? $exception->getMessage() : lang('failed')), 'error');
		}
		else
		{
			$content['output'] .= lang('OAuth Authentiction').': '.($exception ? $exception->getMessage() : lang('failed'));
			$content['connected'] = false;

			$obj->autoconfig($content);
		}
		$obj->autoconfig($content);
	}

	/**
	 * Reorder SSL types to make sure we start with TLS, SSL, STARTTLS and insecure last
	 *
	 * @param array $data ssl => port pairs plus other data like value for 'username'
	 * @return array
	 */
	protected static function fix_ssl_order($data)
	{
		$ordered = array();
		foreach(array_merge(array('TLS', 'SSL', 'STARTTLS'), array_keys($data)) as $key)
		{
			if (array_key_exists($key, $data)) $ordered[$key] = $data[$key];
		}
		return $ordered;
	}

	/**
	 * Query Mozilla's ISPDB
	 *
	 * Some providers eg. 1-and-1 do not report their hosted domains to ISPDB,
	 * therefore we try it with the found MX and it's domain-part (host-name removed).
	 *
	 * @param string $domain domain or email
	 * @param boolean $try_mx =true if domain itself is not found, try mx or domain-part (host removed) of mx
	 * @return array with values for keys 'displayName', 'imap', 'smtp', 'pop3', which each contain
	 *	array of arrays with values for keys 'hostname', 'port', 'socketType'=(SSL|STARTTLS), 'username'=%EMAILADDRESS%
	 */
	protected static function mozilla_ispdb($domain, $try_mx=true)
	{
		if (strpos($domain, '@') !== false) list(,$domain) = explode('@', $domain);

		$url = 'https://autoconfig.thunderbird.net/v1.1/'.$domain;
		try {
			$xml = simplexml_load_string(file_get_contents($url) ?: '');
			if (!$xml || !$xml->emailProvider) throw new Api\Exception\NotFound();
			$provider = array(
				'displayName' => (string)$xml->emailProvider->displayName,
			);
			foreach($xml->emailProvider->children() as $tag => $server)
			{
				if (!in_array($tag, array('incomingServer', 'outgoingServer'))) continue;
				foreach($server->attributes() as $name => $value)
				{
					if ($name == 'type') $type = (string)$value;
				}
				$data = array();
				foreach($server as $name => $value)
				{
					foreach($value->children() as $tag => $val)
					{
						$data[$name][$tag] = (string)$val;
					}
					if (!isset($data[$name])) $data[$name] = (string)$value;
				}
				$provider[$type][] = $data;
			}
		}
		catch(Exception $e) {
			// ignore own not-found exception or xml parsing execptions
			unset($e);

			if ($try_mx && ($dns = dns_get_record($domain, DNS_MX)))
			{
				$domain = $dns[0]['target'];
				if (!($provider = self::mozilla_ispdb($domain, false)))
				{
					list(,$domain) = explode('.', $domain, 2);
					$provider = self::mozilla_ispdb($domain, false);
				}
			}
			else
			{
				$provider = array();
			}
		}
		//error_log(__METHOD__."('$email') returning ".array2string($provider));
		return $provider;
	}

	/**
	 * Guess possible server hostnames from email address:
	 *	- $type.$domain, mail.$domain
	 *  - replace host in MX with imap or mail
	 *  - MX for $domain
	 *
	 * @param string $email email address
	 * @param string $type ='imap' 'imap' or 'smtp', used as hostname beside 'mail'
	 * @return array of hostname => true pairs
	 */
	protected function guess_hosts($email, $type='imap')
	{
		list(,$domain) = explode('@', $email);

		$hosts = array();

		// try usuall names
		$hosts[$type.'.'.$domain] = true;
		$hosts['mail.'.$domain] = true;
		if ($type == 'smtp') $hosts['send.'.$domain] = true;

		if (($dns = dns_get_record($domain, DNS_MX)))
		{
			//error_log(__METHOD__."('$email') dns_get_record('$domain', DNS_MX) returned ".array2string($dns));
			// hosts for office365 are outlook|smpt.office365.com for MX *.mail.protection.outlook.com
			if (substr($dns[0]['target'], -28) == '.mail.protection.outlook.com')
			{
				$hosts[($type == 'imap' ? 'outlook' : 'smtp').'.office365.com'] = true;
			}
			$hosts[preg_replace('/^[^.]+/', $type, $dns[0]['target'])] = true;
			$hosts[preg_replace('/^[^.]+/', 'mail', $dns[0]['target'])] = true;
			if ($type == 'smtp') $hosts[preg_replace('/^[^.]+/', 'send', $dns[0]['target'])] = true;
			$hosts[$dns[0]['target']] = true;
		}

		// verify hosts in dns
		foreach(array_keys($hosts) as $host)
		{
			if (!dns_get_record($host, DNS_A)) unset($hosts[$host]);
		}
		//error_log(__METHOD__."('$email') returning ".array2string($hosts));
		return $hosts;
	}

	/**
	 * Set mail account status wheter to 'active' or '' (inactive)
	 *
	 * @param array $_data account an array of data called via long task running dialog
	 *	$_data:array (
	 *		id => account_id,
	 *		quota => quotaLimit,
	 *		domain => mailLocalAddress,
	 *		status => mail activation status('active'|'')
	 *	)
	 * @param string $etemplate_exec_id to check against CSRF
	 * @return json response
	 */
	public function ajax_activeAccounts($_data, $etemplate_exec_id)
	{
		Api\Etemplate\Request::csrfCheck($etemplate_exec_id, __METHOD__, func_get_args());

		if (!$this->is_admin) die('no rights to be here!');
		$response = Api\Json\Response::get();
		if (($account = $GLOBALS['egw']->accounts->read($_data['id'])))
		{
			if ($_data['quota'] !== '' || $_data['accountStatus'] !== '' || strpos($_data['domain'], '.'))
			{
				$ea_account = Mail\Account::get_default(false, false, false, true, $_data['id'], true);
				if (!$ea_account || !Mail\Account::is_multiple($ea_account))
				{
					$msg = $account['account_fullname'].' (#'.$_data['id'].'): '.lang('No default account found!');
					return $response->data($msg);
				}

				if ($ea_account && ($userData = $ea_account->getUserData()))
				{
					$userData = array(
						'acc_smtp_type' => $ea_account->acc_smtp_type,
						'accountStatus' => $_data['status'],
						'quotaLimit' => $_data['quota'] ?: $userData['quotaLimit'],
						'mailLocalAddress' => $userData['mailLocalAddress'],
					);

					if (strpos($_data['domain'], '.') !== false)
					{
						$userData['mailLocalAddress'] = preg_replace('/@'.preg_quote($ea_account->acc_domain, '/').'$/', '@'.$_data['domain'], $userData['mailLocalAddress']);

						foreach($userData['mailAlternateAddress'] as &$alias)
						{
							$alias = preg_replace('/@'.preg_quote($ea_account->acc_domain, '/').'$/', '@'.$_data['domain'], $alias);
						}
					}
					// fulfill the saveUserData requirements
					$userData += $ea_account->params;
					$ea_account->saveUserData($_data['id'], $userData);
					$msg = $account['account_fullname'].' (#'.$_data['id'].'): '.
						($userData['accountStatus'] === 'active' ? lang('activated') : lang('deactivated'));
				}
				else
				{
					$msg = lang('No profile defined for user %1', $account['account_fullname'].' (#'.$_data['id'].")\n");
				}
			}
		}
		$response->data($msg);
	}
}

/**
 * Trivial file logger, as Horde\ManageSieve does not support just a file
 */
class admin_mail_logger
{
	private $fp;

	public function __construct($log)
	{
		$this->fp = is_resource($log) ? $log : fopen($log, 'a');
	}

	public function debug($msg)
	{
		fwrite($this->fp, $msg."\n");
	}
}