1<?php
2/**
3 * EGroupware EMailAdmin: Wizard to create mail accounts
4 *
5 * @link http://www.egroupware.org
6 * @package emailadmin
7 * @author Ralf Becker <rb@egroupware.org>
8 * @copyright (c) 2013-18 by Ralf Becker <rb@egroupware.org>
9 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
10 */
11
12use EGroupware\Api;
13use EGroupware\Api\Framework;
14use EGroupware\Api\Acl;
15use EGroupware\Api\Etemplate;
16use EGroupware\Api\Mail;
17
18/**
19 * Wizard to create mail accounts
20 *
21 * Wizard uses follow heuristic to search for IMAP accounts:
22 * 1. query Mozilla ISPDB for domain from email (perfering SSL over STARTTLS over insecure connection)
23 * 2. guessing and verifying in DNS server-names based on domain from email:
24 *	- (imap|smtp).$domain, mail.$domain
25 *  - MX is *.mail.protection.outlook.com use (outlook|smtp).office365.com
26 *  - MX for $domain
27 *  - replace host in MX with (imap|smtp) or mail
28 */
29class admin_mail
30{
31	/**
32	 * Enable logging of IMAP communication to given path, eg. /tmp/autoconfig.log
33	 */
34	const DEBUG_LOG = null;
35	/**
36	 * Connection timeout in seconds used in autoconfig, can and should be really short!
37	 */
38	const TIMEOUT = 3;
39	/**
40	 * Prefix for callback names
41	 *
42	 * Used as static::APP_CLASS in etemplate::exec(), to allow mail app extending this class.
43	 */
44	const APP_CLASS = 'admin.admin_mail.';
45
46	/**
47	 * 0: No SSL
48	 */
49	const SSL_NONE = Mail\Account::SSL_NONE;
50	/**
51	 * 1: STARTTLS on regular tcp connection/port
52	 */
53	const SSL_STARTTLS = Mail\Account::SSL_STARTTLS;
54	/**
55	 * 3: SSL (inferior to TLS!)
56	 */
57	const SSL_SSL = Mail\Account::SSL_SSL;
58	/**
59	 * 2: require TLS version 1+, no SSL version 2 or 3
60	 */
61	const SSL_TLS = Mail\Account::SSL_TLS;
62	/**
63	 * 8: if set, verify certifcate (currently not implemented in Horde_Imap_Client!)
64	 */
65	const SSL_VERIFY = Mail\Account::SSL_VERIFY;
66
67	/**
68	 * Log exception including trace to error-log, instead of just displaying the message.
69	 *
70	 * @var boolean
71	 */
72	public static $debug = false;
73
74	/**
75	 * Methods callable via menuaction
76	 *
77	 * @var array
78	 */
79	public $public_functions = array(
80		'add' => true,
81		'edit' => true,
82		'ajax_activeAccounts' => true
83	);
84
85	/**
86	 * Supported ssl types including none
87	 *
88	 * @var array
89	 */
90	public static $ssl_types = array(
91		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
92		self::SSL_SSL => 'SSL',
93		self::SSL_STARTTLS => 'STARTTLS',
94		'no' => 'no',
95	);
96	/**
97	 * Convert ssl-type to Horde secure parameter
98	 *
99	 * @var array
100	 */
101	public static $ssl2secure = array(
102		'SSL' => 'ssl',
103		'STARTTLS' => 'tls',
104		'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
105	);
106	/**
107	 * Convert ssl-type to eMailAdmin acc_(imap|sieve|smtp)_ssl integer value
108	 *
109	 * @var array
110	 */
111	public static $ssl2type = array(
112		'TLS' => self::SSL_TLS,
113		'SSL' => self::SSL_SSL,
114		'STARTTLS' => self::SSL_STARTTLS,
115		'no' => self::SSL_NONE,
116	);
117
118	/**
119	 * Available IMAP login types
120	 *
121	 * @var array
122	 */
123	public static $login_types = array(
124		'' => 'Username specified below for all',
125		'standard'	=> 'username from account',
126		'vmailmgr'	=> 'username@domainname',
127		//'admin'		=> 'Username/Password defined by admin',
128		'uidNumber' => 'UserId@domain eg. u1234@domain',
129		'email'	    => 'EMail-address from account',
130	);
131
132	/**
133	 * Options for further identities
134	 *
135	 * @var array
136	 */
137	public static $further_identities = array(
138		0 => 'Forbid users to create identities',
139		1 => 'Allow users to create further identities',
140		2 => 'Allow users to create identities for aliases',
141	);
142
143	/**
144	 * List of domains know to not support Sieve
145	 *
146	 * Used to switch Sieve off by default, thought users can allways try switching it on.
147	 * Testing not existing Sieve with google takes a long time, as ports are open,
148	 * but not answering ...
149	 *
150	 * @var array
151	 */
152	public static $no_sieve_blacklist = array('gmail.com', 'googlemail.com', 'outlook.office365.com');
153
154	/**
155	 * Is current use a mail administrator / has run rights for EMailAdmin
156	 *
157	 * @var boolean
158	 */
159	protected $is_admin = false;
160
161	/**
162	 * Constructor
163	 */
164	public function __construct()
165	{
166		$this->is_admin = isset($GLOBALS['egw_info']['user']['apps']['admin']);
167
168		// for some reason most translation for account-wizard are in mail
169		Api\Translation::add_app('mail');
170
171		// Horde use locale for translation of error messages
172		Api\Preferences::setlocale(LC_MESSAGES);
173	}
174
175	/**
176	 * Step 1: IMAP account
177	 *
178	 * @param array $content
179	 * @param type $msg
180	 */
181	public function add(array $content=array(), $msg='', $msg_type='success')
182	{
183		$tpl = new Etemplate('admin.mailwizard');
184		if (empty($content['account_id']))
185		{
186			$content['account_id'] = $GLOBALS['egw_info']['user']['account_id'];
187		}
188		// add some defaults if not already set (+= does not overwrite existing values!)
189		$content += array(
190			'ident_realname' => $GLOBALS['egw']->accounts->id2name($content['account_id'], 'account_fullname'),
191			'ident_email' => $GLOBALS['egw']->accounts->id2name($content['account_id'], 'account_email'),
192			'acc_imap_port' => 993,
193			'manual_class' => 'emailadmin_manual',
194		);
195		Framework::message($msg ? $msg : (string)$_GET['msg'], $msg_type);
196
197		if (!empty($content['acc_imap_host']) || !empty($content['acc_imap_username']))
198		{
199			$readonlys['button[manual]'] = true;
200			unset($content['manual_class']);
201		}
202		$tpl->exec(static::APP_CLASS.'autoconfig', $content, array(
203			'acc_imap_ssl' => self::$ssl_types,
204		), $readonlys, $content, 2);
205	}
206
207	/**
208	 * Try to autoconfig an account
209	 *
210	 * @param array $content
211	 */
212	public function autoconfig(array $content)
213	{
214		// user pressed [Skip IMAP] --> jump to SMTP config
215		if ($content['button'] && key($content['button']) == 'skip_imap')
216		{
217			unset($content['button']);
218			if (!isset($content['acc_smtp_host'])) $content['acc_smtp_host'] = '';	// do manual mode right away
219			return $this->smtp($content, lang('Skipping IMAP configuration!'));
220		}
221		$content['output'] = '';
222		$sel_options = $readonlys = array();
223
224		$content['connected'] = $connected = false;
225		if (empty($content['acc_imap_username']))
226		{
227			$content['acc_imap_username'] = $content['ident_email'];
228		}
229		if (!empty($content['acc_imap_host']))
230		{
231			$hosts = array($content['acc_imap_host'] => true);
232			if ($content['acc_imap_port'] > 0 && !in_array($content['acc_imap_port'], array(143,993)))
233			{
234				$ssl_type = (string)array_search($content['acc_imap_ssl'], self::$ssl2type);
235				if ($ssl_type === '') $ssl_type = 'insecure';
236				$hosts[$content['acc_imap_host']] = array(
237					$ssl_type => $content['acc_imap_port'],
238				);
239			}
240		}
241		elseif (($ispdb = self::mozilla_ispdb($content['ident_email'])) && count($ispdb['imap']))
242		{
243			$content['ispdb'] = $ispdb;
244			$content['output'] .= lang('Using data from Mozilla ISPDB for provider %1', $ispdb['displayName'])."\n";
245			$hosts = array();
246			foreach($ispdb['imap'] as $server)
247			{
248				if (!isset($hosts[$server['hostname']]))
249				{
250					$hosts[$server['hostname']] = array('username' => $server['username']);
251				}
252				if (strtoupper($server['socketType']) == 'SSL')	// try TLS first
253				{
254					$hosts[$server['hostname']]['TLS'] = $server['port'];
255				}
256				$hosts[$server['hostname']][strtoupper($server['socketType'])] = $server['port'];
257				// make sure we prefer SSL over STARTTLS over insecure
258				if (count($hosts[$server['hostname']]) > 2)
259				{
260					$hosts[$server['hostname']] = self::fix_ssl_order($hosts[$server['hostname']]);
261				}
262			}
263		}
264		else
265		{
266			$hosts = $this->guess_hosts($content['ident_email'], 'imap');
267		}
268
269		// iterate over all hosts and try to connect
270		foreach($hosts as $host => $data)
271		{
272			$content['acc_imap_host'] = $host;
273			// by default we check SSL, STARTTLS and at last an insecure connection
274			if (!is_array($data)) $data = array('TLS' => 993, 'SSL' => 993, 'STARTTLS' => 143, 'insecure' => 143);
275
276			foreach($data as $ssl => $port)
277			{
278				if ($ssl === 'username') continue;
279
280				$content['acc_imap_ssl'] = (int)self::$ssl2type[$ssl];
281
282				$e = null;
283				try {
284					$content['output'] .= "\n".Api\DateTime::to('now', 'H:i:s').": Trying $ssl connection to $host:$port ...\n";
285					$content['acc_imap_port'] = $port;
286
287					$imap = self::imap_client($content, self::TIMEOUT);
288
289					//$content['output'] .= array2string($imap->capability());
290					$imap->login();
291					$content['output'] .= "\n".lang('Successful connected to %1 server%2.', 'IMAP', ' '.lang('and logged in'))."\n";
292					if (!$imap->isSecureConnection())
293					{
294						$content['output'] .= lang('Connection is NOT secure! Everyone can read eg. your credentials.')."\n";
295						$content['acc_imap_ssl'] = 'no';
296					}
297					//$content['output'] .= "\n\n".array2string($imap->capability());
298					$content['connected'] = $connected = true;
299					break 2;
300				}
301				catch(Horde_Imap_Client_Exception $e)
302				{
303					switch($e->getCode())
304					{
305						case Horde_Imap_Client_Exception::LOGIN_AUTHENTICATIONFAILED:
306							$content['output'] .= "\n".$e->getMessage()."\n";
307							break 3;	// no need to try other SSL or non-SSL connections, if auth failed
308
309						case Horde_Imap_Client_Exception::SERVER_CONNECT:
310							$content['output'] .= "\n".$e->getMessage()."\n";
311							if ($ssl == 'STARTTLS') break 2;	// no need to try insecure connection on same port
312							break;
313
314						default:
315							$content['output'] .= "\n".get_class($e).': '.$e->getMessage().' ('.$e->getCode().')'."\n";
316							//$content['output'] .= $e->getTraceAsString()."\n";
317					}
318					if (self::$debug) _egw_log_exception($e);
319				}
320				catch(Exception $e) {
321					$content['output'] .= "\n".get_class($e).': '.$e->getMessage().' ('.$e->getCode().')'."\n";
322					//$content['output'] .= $e->getTraceAsString()."\n";
323					if (self::$debug) _egw_log_exception($e);
324				}
325			}
326		}
327		if ($connected)	// continue with next wizard step: define folders
328		{
329			unset($content['button']);
330			return $this->folder($content, lang('Successful connected to %1 server%2.', 'IMAP', ' '.lang('and logged in')).
331				($imap->isSecureConnection() ? '' : "\n".lang('Connection is NOT secure! Everyone can read eg. your credentials.')));
332		}
333		// add validation error, if we can identify a field
334		if (!$connected && $e instanceof Horde_Imap_Client_Exception)
335		{
336			switch($e->getCode())
337			{
338				case Horde_Imap_Client_Exception::LOGIN_AUTHENTICATIONFAILED:
339					Etemplate::set_validation_error('acc_imap_username', lang($e->getMessage()));
340					Etemplate::set_validation_error('acc_imap_password', lang($e->getMessage()));
341					break;
342
343				case Horde_Imap_Client_Exception::SERVER_CONNECT:
344					Etemplate::set_validation_error('acc_imap_host', lang($e->getMessage()));
345					break;
346			}
347		}
348		$readonlys['button[manual]'] = true;
349		unset($content['manual_class']);
350		$sel_options['acc_imap_ssl'] = self::$ssl_types;
351		$tpl = new Etemplate('admin.mailwizard');
352		$tpl->exec(static::APP_CLASS.'autoconfig', $content, $sel_options, $readonlys, $content, 2);
353	}
354
355	/**
356	 * Step 2: Folder - let user select trash, sent, drafs and template folder
357	 *
358	 * @param array $content
359	 * @param string $msg =''
360	 * @param Horde_Imap_Client_Socket $imap =null
361	 */
362	public function folder(array $content, $msg='', Horde_Imap_Client_Socket $imap=null)
363	{
364		if (isset($content['button']))
365		{
366			$button = key($content['button']);
367			unset($content['button']);
368			switch($button)
369			{
370				case 'back':
371					return $this->add($content);
372
373				case 'continue':
374					return $this->sieve($content);
375			}
376		}
377		$content['msg'] = $msg;
378		if (!isset($imap)) $imap = self::imap_client ($content);
379
380		try {
381			//_debug_array($content);
382			$sel_options['acc_folder_sent'] = $sel_options['acc_folder_trash'] =
383				$sel_options['acc_folder_draft'] = $sel_options['acc_folder_template'] =
384					$sel_options['acc_folder_junk'] = $sel_options['acc_folder_archive'] =
385						$sel_options['acc_folder_ham'] = self::mailboxes($imap, $content);
386		}
387		catch(Exception $e) {
388			$content['msg'] = $e->getMessage();
389			if (self::$debug) _egw_log_exception($e);
390		}
391
392		$tpl = new Etemplate('admin.mailwizard.folder');
393		$tpl->exec(static::APP_CLASS.'folder', $content, $sel_options, array(), $content);
394	}
395
396	/**
397	 * Query mailboxes and (optional) detect special folders
398	 *
399	 * @param Horde_Imap_Client_Socket $imap
400	 * @param array &$content=null on return values for acc_folder_(sent|trash|draft|template)
401	 * @return array with folders as key AND value
402	 * @throws Horde_Imap_Client_Exception
403	 */
404	public static function mailboxes(Horde_Imap_Client_Socket $imap, array &$content=null)
405	{
406		// query all subscribed mailboxes
407		$mailboxes = $imap->listMailboxes('*', Horde_Imap_Client::MBOX_SUBSCRIBED, array(
408			'special_use' => true,
409			'attributes' => true,	// otherwise special_use is only queried, but not returned ;-)
410			'delimiter' => true,
411		));
412		//_debug_array($mailboxes);
413		// list mailboxes by special-use attributes
414		$folders = $attributes = $all = array();
415		foreach($mailboxes as $mailbox => $data)
416		{
417			foreach($data['attributes'] as $attribute)
418			{
419				$attributes[$attribute][] = $mailbox;
420			}
421			$folders[$mailbox] = $mailbox.': '.implode(', ', $data['attributes']);
422		}
423		// pre-select send, trash, ... folder for user, by checking special-use attributes or common name(s)
424		foreach(array(
425			'acc_folder_sent'  => array('\\sent', 'sent'),
426			'acc_folder_trash' => array('\\trash', 'trash'),
427			'acc_folder_draft' => array('\\drafts', 'drafts'),
428			'acc_folder_template' => array('', 'templates'),
429			'acc_folder_junk'  => array('\\junk', 'junk', 'spam'),
430			'acc_folder_ham'   => array('', 'ham'),
431			'acc_folder_archive' => array('', 'archive'),
432		) as $name => $common_names)
433		{
434			// first check special-use attributes
435			if (($special_use = array_shift($common_names)))
436			{
437				foreach((array)$attributes[$special_use] as $mailbox)
438				{
439					if (empty($content[$name]) || strlen($mailbox) < strlen($content[$name]))
440					{
441						$content[$name] = $mailbox;
442					}
443				}
444			}
445			// no special use folder found, try common names
446			if (empty($content[$name]))
447			{
448				foreach($mailboxes as $mailbox => $data)
449				{
450					$delimiter = !empty($data['delimiter']) ? $data['delimiter'] : '.';
451					$name_parts = explode($delimiter, strtolower($mailbox));
452					if (array_intersect($name_parts, $common_names) &&
453						(empty($content[$name]) || strlen($mailbox) < strlen($content[$name]) && substr($content[$name], 0, 6) != 'INBOX'.$delimiter))
454					{
455						//error_log(__METHOD__."() $mailbox --> ".substr($name, 11).' folder');
456						$content[$name] = $mailbox;
457					}
458					//else error_log(__METHOD__."() $mailbox does NOT match array_intersect(".array2string($name_parts).', '.array2string($common_names).')='.array2string(array_intersect($name_parts, $common_names)));
459				}
460			}
461			$folders[(string)$content[$name]] .= ' --> '.substr($name, 11).' folder';
462		}
463		// uncomment for infos about selection process
464		//$content['folder_output'] = implode("\n", $folders);
465
466		return array_combine(array_keys($mailboxes), array_keys($mailboxes));
467	}
468
469	/**
470	 * Step 3: Sieve
471	 *
472	 * @param array $content
473	 * @param string $msg =''
474	 */
475	public function sieve(array $content, $msg='')
476	{
477		static $sieve_ssl2port = array(
478			self::SSL_TLS => 5190,
479			self::SSL_SSL => 5190,
480			self::SSL_STARTTLS => array(4190, 2000),
481			self::SSL_NONE => array(4190, 2000),
482		);
483		$content['msg'] = $msg;
484
485		if (isset($content['button']))
486		{
487			$button = key($content['button']);
488			unset($content['button']);
489			switch($button)
490			{
491				case 'back':
492					return $this->folder($content);
493
494				case 'continue':
495					if (!$content['acc_sieve_enabled'])
496					{
497						return $this->smtp($content);
498					}
499					break;
500			}
501		}
502		// first try: hide manual config
503		if (!isset($content['acc_sieve_enabled']))
504		{
505			list(, $domain) = explode('@', $content['acc_imap_username']);
506			$content['acc_sieve_enabled'] = (int)!in_array($domain, self::$no_sieve_blacklist);
507			$content['manual_class'] = 'emailadmin_manual';
508		}
509		else
510		{
511			unset($content['manual_class']);
512			$readonlys['button[manual]'] = true;
513		}
514		// set default ssl and port
515		if (!isset($content['acc_sieve_ssl'])) $content['acc_sieve_ssl'] = key(self::$ssl_types);
516		if (empty($content['acc_sieve_port'])) $content['acc_sieve_port'] = $sieve_ssl2port[$content['acc_sieve_ssl']];
517
518		// check smtp connection
519		if ($button == 'continue')
520		{
521			$content['sieve_connected'] = false;
522			$content['sieve_output'] = '';
523			unset($content['manual_class']);
524
525			if (empty($content['acc_sieve_host']))
526			{
527				$content['acc_sieve_host'] = $content['acc_imap_host'];
528			}
529			// if use set non-standard port, use it
530			if (!in_array($content['acc_sieve_port'], (array)$sieve_ssl2port[$content['acc_sieve_ssl']]))
531			{
532				$data = array($content['acc_sieve_ssl'] => $content['acc_sieve_port']);
533			}
534			else	// otherwise try all standard ports
535			{
536				$data = $sieve_ssl2port;
537			}
538			foreach($data as $ssl => $ports)
539			{
540				foreach((array)$ports as $port)
541				{
542					$content['acc_sieve_ssl'] = $ssl;
543					$ssl_label = self::$ssl_types[$ssl];
544
545					$e = null;
546					try {
547						$content['sieve_output'] .= "\n".Api\DateTime::to('now', 'H:i:s').": Trying $ssl_label connection to $content[acc_sieve_host]:$port ...\n";
548						$content['acc_sieve_port'] = $port;
549						$sieve = new Horde\ManageSieve(array(
550							'host' => $content['acc_sieve_host'],
551							'port' => $content['acc_sieve_port'],
552							'secure' => self::$ssl2secure[(string)array_search($content['acc_sieve_ssl'], self::$ssl2type)],
553							'timeout' => self::TIMEOUT,
554							'logger' => self::DEBUG_LOG ? new admin_mail_logger(self::DEBUG_LOG) : null,
555						));
556						// connect to sieve server
557						$sieve->connect();
558						$content['sieve_output'] .= "\n".lang('Successful connected to %1 server%2.', 'Sieve','');
559						// and log in
560						$sieve->login($content['acc_imap_username'], $content['acc_imap_password']);
561						$content['sieve_output'] .= ' '.lang('and logged in')."\n";
562						$content['sieve_connected'] = true;
563
564						unset($content['button']);
565						return $this->smtp($content, lang('Successful connected to %1 server%2.', 'Sieve',
566							' '.lang('and logged in')));
567					}
568					catch(Horde\ManageSieve\Exception\ConnectionFailed $e) {
569						$content['sieve_output'] .= "\n".$e->getMessage().' '.$e->details."\n";
570					}
571					catch(Exception $e) {
572						$content['sieve_output'] .= "\n".get_class($e).': '.$e->getMessage().
573							($e->details ? ' '.$e->details : '').' ('.$e->getCode().')'."\n";
574						$content['sieve_output'] .= $e->getTraceAsString()."\n";
575						if (self::$debug) _egw_log_exception($e);
576					}
577				}
578			}
579			// not connected, and default ssl/port --> reset again to secure settings
580			if ($data == $sieve_ssl2port)
581			{
582				$content['acc_sieve_ssl'] = key(self::$ssl_types);
583				$content['acc_sieve_port'] = $sieve_ssl2port[$content['acc_sieve_ssl']];
584			}
585		}
586		// add validation error, if we can identify a field
587		if (!$content['sieve_connected'] && $e instanceof Exception)
588		{
589			switch($e->getCode())
590			{
591				case 61:	// connection refused
592				case 60:	// connection timed out (imap.googlemail.com returns that for none-ssl/4190/2000)
593				case 65:	// no route ot host (imap.googlemail.com returns that for ssl/5190)
594					Etemplate::set_validation_error('acc_sieve_host', lang($e->getMessage()));
595					Etemplate::set_validation_error('acc_sieve_port', lang($e->getMessage()));
596					break;
597			}
598			$content['msg'] = lang('No sieve support detected, either fix configuration manually or leave it switched off.');
599			$content['acc_sieve_enabled'] = 0;
600		}
601		$sel_options['acc_sieve_ssl'] = self::$ssl_types;
602		$tpl = new Etemplate('admin.mailwizard.sieve');
603		$tpl->exec(static::APP_CLASS.'sieve', $content, $sel_options, $readonlys, $content, 2);
604	}
605
606	/**
607	 * Step 4: SMTP
608	 *
609	 * @param array $content
610	 * @param string $msg =''
611	 */
612	public function smtp(array $content, $msg='')
613	{
614		static $smtp_ssl2port = array(
615			self::SSL_NONE => 25,
616			self::SSL_SSL => 465,
617			self::SSL_TLS => 465,
618			self::SSL_STARTTLS => 587,
619		);
620		$content['msg'] = $msg;
621
622		if (isset($content['button']))
623		{
624			$button = key($content['button']);
625			unset($content['button']);
626			switch($button)
627			{
628				case 'back':
629					return $this->sieve($content);
630			}
631		}
632		// first try: hide manual config
633		if (!isset($content['acc_smtp_host']))
634		{
635			$content['manual_class'] = 'emailadmin_manual';
636		}
637		else
638		{
639			unset($content['manual_class']);
640			$readonlys['button[manual]'] = true;
641		}
642		// copy username/password from imap
643		if (!isset($content['acc_smtp_username'])) $content['acc_smtp_username'] = $content['acc_imap_username'];
644		if (!isset($content['acc_smtp_password'])) $content['acc_smtp_password'] = $content['acc_imap_password'];
645		// set default ssl
646		if (!isset($content['acc_smtp_ssl'])) $content['acc_smtp_ssl'] = key(self::$ssl_types);
647		if (empty($content['acc_smtp_port'])) $content['acc_smtp_port'] = $smtp_ssl2port[$content['acc_smtp_ssl']];
648
649		// check smtp connection
650		if ($button == 'continue')
651		{
652			$content['smtp_connected'] = false;
653			$content['smtp_output'] = '';
654			unset($content['manual_class']);
655
656			if (!empty($content['acc_smtp_host']))
657			{
658				$hosts = array($content['acc_smtp_host'] => true);
659				if ((string)$content['acc_smtp_ssl'] !== (string)self::SSL_TLS || $content['acc_smtp_port'] != $smtp_ssl2port[$content['acc_smtp_ssl']])
660				{
661					$ssl_type = (string)array_search($content['acc_smtp_ssl'], self::$ssl2type);
662					$hosts[$content['acc_smtp_host']] = array(
663						$ssl_type => $content['acc_smtp_port'],
664					);
665				}
666			}
667			elseif($content['ispdb'] && !empty($content['ispdb']['smtp']))
668			{
669				$content['smtp_output'] .= lang('Using data from Mozilla ISPDB for provider %1', $content['ispdb']['displayName'])."\n";
670				$hosts = array();
671				foreach($content['ispdb']['smtp'] as $server)
672				{
673					if (!isset($hosts[$server['hostname']]))
674					{
675						$hosts[$server['hostname']] = array('username' => $server['username']);
676					}
677					if (strtoupper($server['socketType']) == 'SSL')	// try TLS first
678					{
679						$hosts[$server['hostname']]['TLS'] = $server['port'];
680					}
681					$hosts[$server['hostname']][strtoupper($server['socketType'])] = $server['port'];
682					// make sure we prefer SSL over STARTTLS over insecure
683					if (count($hosts[$server['hostname']]) > 2)
684					{
685						$hosts[$server['hostname']] = self::fix_ssl_order($hosts[$server['hostname']]);
686					}
687				}
688			}
689			else
690			{
691				$hosts = $this->guess_hosts($content['ident_email'], 'smtp');
692			}
693			foreach($hosts as $host => $data)
694			{
695				$content['acc_smtp_host'] = $host;
696				if (!is_array($data))
697				{
698					$data = array('TLS' => 465, 'SSL' => 465, 'STARTTLS' => 587, '' => 25);
699				}
700				foreach($data as $ssl => $port)
701				{
702					if ($ssl === 'username') continue;
703
704					$content['acc_smtp_ssl'] = (int)self::$ssl2type[$ssl];
705
706					$e = null;
707					try {
708						$content['smtp_output'] .= "\n".Api\DateTime::to('now', 'H:i:s').": Trying $ssl connection to $host:$port ...\n";
709						$content['acc_smtp_port'] = $port;
710
711						$mail = new Horde_Mail_Transport_Smtphorde($params=array(
712							'username' => $content['acc_smtp_username'],
713							'password' => $content['acc_smtp_password'],
714							'host' => $content['acc_smtp_host'],
715							'port' => $content['acc_smtp_port'],
716							'secure' => self::$ssl2secure[(string)array_search($content['acc_smtp_ssl'], self::$ssl2type)],
717							'timeout' => self::TIMEOUT,
718							'debug' => self::DEBUG_LOG,
719						));
720						// create smtp connection and authenticate, if credentials given
721						$smtp = $mail->getSMTPObject();
722						$content['smtp_output'] .= "\n".lang('Successful connected to %1 server%2.', 'SMTP',
723							(!empty($content['acc_smtp_username']) ? ' '.lang('and logged in') : ''))."\n";
724						if (!$smtp->isSecureConnection())
725						{
726							if (!empty($content['acc_smtp_username']))
727							{
728								$content['smtp_output'] .= lang('Connection is NOT secure! Everyone can read eg. your credentials.')."\n";
729							}
730							$content['acc_smtp_ssl'] = 'no';
731						}
732						// Horde_Smtp always try to use STARTTLS, adjust our ssl-parameter if successful
733						elseif (!($content['acc_smtp_ssl'] > self::SSL_NONE))
734						{
735							//error_log(__METHOD__."() new Horde_Mail_Transport_Smtphorde(".array2string($params).")->getSMTPObject()->isSecureConnection()=".array2string($smtp->isSecureConnection()));
736							$content['acc_smtp_ssl'] = self::SSL_STARTTLS;
737						}
738						// try sending a mail to a different domain, if not authenticated, to see if that's required
739						if (empty($content['acc_smtp_username']))
740						{
741							$smtp->send($content['ident_email'], 'noreply@example.com', '');
742							$content['smtp_output'] .= "\n".lang('Relay access checked')."\n";
743						}
744						$content['smtp_connected'] = true;
745						unset($content['button']);
746						return $this->edit($content, lang('Successful connected to %1 server%2.', 'SMTP',
747							empty($content['acc_smtp_username']) ? ' - '.lang('Relay access checked') : ' '.lang('and logged in')));
748					}
749					// unfortunately LOGIN_AUTHENTICATIONFAILED and SERVER_CONNECT are thrown as Horde_Mail_Exception
750					// while others are thrown as Horde_Smtp_Exception --> using common base Horde_Exception_Wrapped
751					catch(Horde_Exception_Wrapped $e)
752					{
753						switch($e->getCode())
754						{
755							case Horde_Smtp_Exception::LOGIN_AUTHENTICATIONFAILED:
756							case Horde_Smtp_Exception::LOGIN_REQUIREAUTHENTICATION:
757							case Horde_Smtp_Exception::UNSPECIFIED:
758								$content['smtp_output'] .= "\n".$e->getMessage()."\n";
759								break;
760							case Horde_Smtp_Exception::SERVER_CONNECT:
761								$content['smtp_output'] .= "\n".$e->getMessage()."\n";
762								break;
763							default:
764								$content['smtp_output'] .= "\n".$e->getMessage().' ('.$e->getCode().')'."\n";
765								break;
766						}
767						if (self::$debug) _egw_log_exception($e);
768					}
769					catch(Horde_Smtp_Exception $e)
770					{
771						// prever $e->details over $e->getMessage() as it contains original message from SMTP server (eg. relay access denied)
772						$content['smtp_output'] .= "\n".(empty($e->details) ? $e->getMessage().' ('.$e->getCode().')' : $e->details)."\n";
773						//$content['smtp_output'] .= $e->getTraceAsString()."\n";
774						if (self::$debug) _egw_log_exception($e);
775					}
776					catch(Exception $e) {
777						$content['smtp_output'] .= "\n".get_class($e).': '.$e->getMessage().' ('.$e->getCode().')'."\n";
778						//$content['smtp_output'] .= $e->getTraceAsString()."\n";
779						if (self::$debug) _egw_log_exception($e);
780					}
781				}
782			}
783		}
784		// add validation error, if we can identify a field
785		if (!$content['smtp_connected'] && $e instanceof Horde_Exception_Wrapped)
786		{
787			switch($e->getCode())
788			{
789				case Horde_Smtp_Exception::LOGIN_AUTHENTICATIONFAILED:
790				case Horde_Smtp_Exception::LOGIN_REQUIREAUTHENTICATION:
791				case Horde_Smtp_Exception::UNSPECIFIED:
792					Etemplate::set_validation_error('acc_smtp_username', lang($e->getMessage()));
793					Etemplate::set_validation_error('acc_smtp_password', lang($e->getMessage()));
794					break;
795
796				case Horde_Smtp_Exception::SERVER_CONNECT:
797					Etemplate::set_validation_error('acc_smtp_host', lang($e->getMessage()));
798					Etemplate::set_validation_error('acc_smtp_port', lang($e->getMessage()));
799					break;
800			}
801		}
802		$sel_options['acc_smtp_ssl'] = self::$ssl_types;
803		$tpl = new Etemplate('admin.mailwizard.smtp');
804		$tpl->exec(static::APP_CLASS.'smtp', $content, $sel_options, $readonlys, $content, 2);
805	}
806
807	/**
808	 * Edit mail account(s)
809	 *
810	 * Gets either called with GET parameter:
811	 *
812	 * a) account_id from admin >> Manage users to edit / add mail accounts for a user
813	 *    --> shows selectbox to switch between different mail accounts of user and "create new account"
814	 *
815	 * b) via mail_wizard proxy class by regular mail user to edit (acc_id GET parameter) or create new mail account
816	 *
817	 * @param array $content =null
818	 * @param string $msg =''
819	 * @param string $msg_type ='success'
820	 */
821	public function edit(array $content=null, $msg='', $msg_type='success')
822	{
823		// app is trying to tell something, while redirecting to wizard
824		if (empty($content) && $_GET['acc_id'] && empty($msg) && !empty( $_GET['msg']))
825		{
826			if (stripos($_GET['msg'],'fatal error:')!==false || $_GET['msg_type'] == 'error') $msg_type = 'error';
827		}
828		if ($content['acc_id'] || (isset($_GET['acc_id']) && (int)$_GET['acc_id'] > 0) ) Mail::unsetCachedObjects($content['acc_id']?$content['acc_id']:$_GET['acc_id']);
829		$tpl = new Etemplate('admin.mailaccount');
830
831		if (!is_array($content) || !empty($content['acc_id']) && isset($content['old_acc_id']) && $content['acc_id'] != $content['old_acc_id'])
832		{
833			if (!is_array($content)) $content = array();
834			if ($this->is_admin && isset($_GET['account_id']))
835			{
836				$content['called_for'] = (int)$_GET['account_id'];
837				$content['accounts'] = iterator_to_array(Mail\Account::search($content['called_for']));
838				if ($content['accounts'])
839				{
840					$content['acc_id'] = key($content['accounts']);
841					//error_log(__METHOD__.__LINE__.'.'.array2string($content['acc_id']));
842					// test if the "to be selected" acccount is imap or not
843					if (is_array($content['accounts']) && count($content['accounts'])>1 && Mail\Account::is_multiple($content['acc_id']))
844					{
845						try {
846							$account = Mail\Account::read($content['acc_id'], $content['called_for']);
847							//try to select the first account that is of type imap
848							if (!$account->is_imap())
849							{
850								$content['acc_id'] = key($content['accounts']);
851								//error_log(__METHOD__.__LINE__.'.'.array2string($content['acc_id']));
852							}
853						}
854						catch(Api\Exception\NotFound $e) {
855							if (self::$debug) _egw_log_exception($e);
856						}
857					}
858				}
859				if (!$content['accounts'])	// no email account, call wizard
860				{
861					return $this->add(array('account_id' => (int)$_GET['account_id']));
862				}
863				$content['accounts']['new'] = lang('Create new account');
864			}
865			if (isset($_GET['acc_id']) && (int)$_GET['acc_id'] > 0)
866			{
867				$content['acc_id'] = (int)$_GET['acc_id'];
868			}
869			// clear current account-data, as account has changed and we going to read selected one
870			$content = array_intersect_key($content, array_flip(array('called_for', 'accounts', 'acc_id', 'tabs')));
871
872			if ($content['acc_id'] > 0)
873			{
874				try {
875					$account = Mail\Account::read($content['acc_id'], $this->is_admin && $content['called_for'] ?
876						$content['called_for'] : $GLOBALS['egw_info']['user']['account_id']);
877					$account->getUserData();	// quota, aliases, forwards etc.
878					$content += $account->params;
879					$content['acc_sieve_enabled'] = (string)($content['acc_sieve_enabled']);
880					$content['notify_use_default'] = !$content['notify_account_id'];
881					self::fix_account_id_0($content['account_id']);
882
883					// read identities (of current user) and mark std identity
884					$content['identities'] = iterator_to_array(Mail\Account::identities($account, true, 'name', $content['called_for']));
885					$content['std_ident_id'] = $content['ident_id'];
886					$content['identities'][$content['std_ident_id']] = lang('Standard identity');
887					// change self::SSL_NONE (=0) to "no" used in sel_options
888					foreach(array('imap','smtp','sieve') as $type)
889					{
890						if (!$content['acc_'.$type.'_ssl']) $content['acc_'.$type.'_ssl'] = 'no';
891					}
892				}
893				catch(Api\Exception\NotFound $e) {
894					if (self::$debug) _egw_log_exception($e);
895					Framework::window_close(lang('Account not found!'));
896				}
897				catch(Exception $e) {
898					if (self::$debug) _egw_log_exception($e);
899					Framework::window_close($e->getMessage().' ('.get_class($e).': '.$e->getCode().')');
900				}
901			}
902			elseif ($content['acc_id'] === 'new')
903			{
904				$content['account_id'] = $content['called_for'];
905				$content['old_acc_id'] = $content['acc_id'];	// to not call add/wizard, if we return from to
906				unset($content['tabs']);
907				return $this->add($content);
908			}
909		}
910		// some defaults for new accounts
911		if (!isset($content['account_id']) || empty($content['acc_id']) || $content['acc_id'] === 'new')
912		{
913			if (!isset($content['account_id'])) $content['account_id'] = array($GLOBALS['egw_info']['user']['account_id']);
914			$content['acc_user_editable'] = $content['acc_further_identities'] = true;
915			$readonlys['ident_id'] = true;	// need to create standard identity first
916		}
917		if (empty($content['acc_name']))
918		{
919			$content['acc_name'] = $content['ident_email'];
920		}
921		// disable some stuff for non-emailadmins (all values are preserved!)
922		if (!$this->is_admin)
923		{
924			$readonlys = array(
925				'account_id' => true, 'button[multiple]' => true, 'acc_user_editable' => true,
926				'acc_further_identities' => true,
927				'acc_imap_type' => true, 'acc_imap_logintype' => true, 'acc_domain' => true,
928				'acc_imap_admin_username' => true, 'acc_imap_admin_password' => true,
929				'acc_smtp_type' => true, 'acc_smtp_auth_session' => true,
930			);
931		}
932		// ensure correct values for single user mail accounts (we only hide them client-side)
933		if (!($is_multiple = Mail\Account::is_multiple($content)))
934		{
935			$content['acc_imap_type'] = 'EGroupware\\Api\\Mail\\Imap';
936			unset($content['acc_imap_login_type']);
937			$content['acc_smtp_type'] = 'EGroupware\\Api\\Mail\\Smtp';
938			unset($content['acc_smtp_auth_session']);
939			unset($content['notify_use_default']);
940		}
941		// copy ident_email_alias selectbox back to regular name
942		elseif (isset($content['ident_email_alias']) && !empty ($content['ident_email_alias']))
943		{
944			$content['ident_email'] = $content['ident_email_alias'];
945		}
946		$edit_access = Mail\Account::check_access(Acl::EDIT, $content);
947
948		// disable notification save-default and use-default, if only one account or no edit-rights
949		$tpl->disableElement('notify_save_default', !$is_multiple || !$edit_access);
950		$tpl->disableElement('notify_use_default', !$is_multiple);
951
952		if (isset($content['button']))
953		{
954			$button = key($content['button']);
955			unset($content['button']);
956			switch($button)
957			{
958				case 'wizard':
959					// if we just came from wizard, go back to last page/step
960					if (isset($content['smtp_connected']))
961					{
962						return $this->smtp($content);
963					}
964					// otherwise start with first step
965					return $this->autoconfig($content);
966
967				case 'delete_identity':
968					// delete none-standard identity of current user
969					if (($this->is_admin || $content['acc_further_identities']) &&
970						$content['ident_id'] > 0 && $content['std_ident_id'] != $content['ident_id'])
971					{
972						Mail\Account::delete_identity($content['ident_id']);
973						$msg = lang('Identity deleted');
974						unset($content['identities'][$content['ident_id']]);
975						$content['ident_id'] = $content['std_ident_id'];
976					}
977					break;
978
979				case 'save':
980				case 'apply':
981					try {
982						// save none-standard identity for current user
983						if ($content['acc_id'] && $content['acc_id'] !== 'new' &&
984							($this->is_admin || $content['acc_further_identities']) &&
985							$content['std_ident_id'] != $content['ident_id'])
986						{
987							$content['ident_id'] = Mail\Account::save_identity(array(
988								'account_id' => $content['called_for'] ? $content['called_for'] : $GLOBALS['egw_info']['user']['account_id'],
989							)+$content);
990							$content['identities'][$content['ident_id']] = Mail\Account::identity_name($content);
991							$msg = lang('Identity saved.');
992							if ($edit_access) $msg .= ' '.lang('Switch back to standard identity to save account.');
993						}
994						elseif ($edit_access)
995						{
996							// if admin username/password given, check if it is valid
997							$account = new Mail\Account($content);
998							if ($account->acc_imap_administration)
999							{
1000								$imap = $account->imapServer(true);
1001								if ($imap) $imap->checkAdminConnection();
1002							}
1003							// test sieve connection, if not called for other user, enabled and credentials available
1004							if (!$content['called_for'] && $account->acc_sieve_enabled && $account->acc_imap_username)
1005							{
1006								$account->imapServer()->retrieveRules();
1007							}
1008							$new_account = !($content['acc_id'] > 0);
1009							// check for deliveryMode="forwardOnly", if a forwarding-address is given
1010							if ($content['acc_smtp_type'] != 'EGroupware\\Api\\Mail\\Smtp' &&
1011								$content['deliveryMode'] == Mail\Smtp::FORWARD_ONLY &&
1012								empty($content['mailForwardingAddress']))
1013							{
1014								Etemplate::set_validation_error('mailForwardingAddress', lang('Field must not be empty !!!'));
1015								throw new Api\Exception\WrongUserinput(lang('You need to specify a forwarding address, when checking "%1"!', lang('Forward only')));
1016							}
1017							// set notifications to store according to checkboxes
1018							if ($content['notify_save_default'])
1019							{
1020								$content['notify_account_id'] = 0;
1021							}
1022							elseif (!$content['notify_use_default'])
1023							{
1024								$content['notify_account_id'] = $content['called_for'] ?
1025									$content['called_for'] : $GLOBALS['egw_info']['user']['account_id'];
1026							}
1027							// SMIME SAVE
1028							if (isset($content['smimeKeyUpload']))
1029							{
1030								$content['acc_smime_cred_id'] = self::save_smime_key($content, $tpl, $content['called_for']);
1031								unset($content['smimeKeyUpload']);
1032							}
1033							self::fix_account_id_0($content['account_id'], true);
1034							$content = Mail\Account::write($content, $content['called_for'] || !$this->is_admin ?
1035								$content['called_for'] : $GLOBALS['egw_info']['user']['account_id']);
1036							self::fix_account_id_0($content['account_id']);
1037							$msg = lang('Account saved.');
1038							// user wants default notifications
1039							if ($content['acc_id'] && $content['notify_use_default'])
1040							{
1041								// delete own ones
1042								Mail\Notifications::delete($content['acc_id'], $content['called_for'] ?
1043									$content['called_for'] : $GLOBALS['egw_info']['user']['account_id']);
1044								// load default ones
1045								$content = array_merge($content, Mail\Notifications::read($content['acc_id'], 0));
1046							}
1047							// add new std identity entry
1048							if ($new_account)
1049							{
1050								$content['std_ident_id'] = $content['ident_id'];
1051								$content['identities'] = array(
1052									$content['std_ident_id'] => lang('Standard identity'));
1053							}
1054							if (isset($content['accounts']))
1055							{
1056								if (!isset($content['accounts'][$content['acc_id']]))	// insert new account as top, not bottom
1057								{
1058									$content['accounts'] = array($content['acc_id'] => '') + $content['accounts'];
1059								}
1060								$content['accounts'][$content['acc_id']] = Mail\Account::identity_name($content, false);
1061							}
1062						}
1063						else
1064						{
1065							if ($content['notify_use_default'] && $content['notify_account_id'])
1066							{
1067								// delete own ones
1068								if (Mail\Notifications::delete($content['acc_id'], $content['called_for'] ?
1069									$content['called_for'] : $GLOBALS['egw_info']['user']['account_id']))
1070								{
1071									$msg = lang('Notification folders updated.');
1072								}
1073								// load default ones
1074								$content = array_merge($content, Mail\Notifications::read($content['acc_id'], 0));
1075							}
1076							if (!$content['notify_use_default'] && is_array($content['notify_folders']))
1077							{
1078								$content['notify_account_id'] = $content['called_for'] ?
1079									$content['called_for'] : $GLOBALS['egw_info']['user']['account_id'];
1080								if (Mail\Notifications::write($content['acc_id'], $content['notify_account_id'],
1081									$content['notify_folders']))
1082								{
1083									$msg = lang('Notification folders updated.');
1084								}
1085							}
1086							if ($content['acc_user_forward'] && !empty($content['acc_smtp_type']) && $content['acc_smtp_type'] != 'EGroupware\\Api\\Mail\\Smtp')
1087							{
1088								$account = new Mail\Account($content);
1089								$account->smtpServer()->saveSMTPForwarding($content['called_for'] ?
1090									$content['called_for'] : $GLOBALS['egw_info']['user']['account_id'],
1091									$content['mailForwardingAddress'],
1092									$content['forwardOnly'] ? null : 'yes');
1093							}
1094							// smime (private) key uploaded by user himself
1095							if (!empty($content['smimeKeyUpload']))
1096							{
1097								$content['acc_smime_cred_id'] = self::save_smime_key($content, $tpl);
1098								unset($content['smimeKeyUpload']);
1099							}
1100						}
1101					}
1102					catch (Horde_Imap_Client_Exception $e)
1103					{
1104						_egw_log_exception($e);
1105						$tpl->set_validation_error('acc_imap_admin_username', $msg=lang($e->getMessage()).($e->details?', '.lang($e->details):''));
1106						$msg_type = 'error';
1107						$content['tabs'] = 'admin.mailaccount.imap';	// should happen automatic
1108						break;
1109					}
1110					catch (Horde\ManageSieve\Exception\ConnectionFailed $e)
1111					{
1112						_egw_log_exception($e);
1113						$tpl->set_validation_error('acc_sieve_port', $msg=lang($e->getMessage()));
1114						$msg_type = 'error';
1115						$content['tabs'] = 'admin.mailaccount.sieve';	// should happen automatic
1116						break;
1117					}
1118					catch (Exception $e) {
1119						$msg = lang('Error saving account!')."\n".$e->getMessage();
1120						$button = 'apply';
1121						$msg_type = 'error';
1122					}
1123					if ($content['acc_id']) Mail::unsetCachedObjects($content['acc_id']);
1124					if (stripos($msg,'fatal error:')!==false) $msg_type = 'error';
1125					Framework::refresh_opener($msg, 'emailadmin', $content['acc_id'], $new_account ? 'add' : 'update', null, null, null, $msg_type);
1126					if ($button == 'save') Framework::window_close();
1127					break;
1128
1129				case 'delete':
1130					if (!Mail\Account::check_access(Acl::DELETE, $content))
1131					{
1132						$msg = lang('Permission denied!');
1133						$msg_type = 'error';
1134					}
1135					elseif (Mail\Account::delete($content['acc_id']) > 0)
1136					{
1137						if ($content['acc_id']) Mail::unsetCachedObjects($content['acc_id']);
1138						Framework::refresh_opener(lang('Account deleted.'), 'emailadmin', $content['acc_id'], 'delete');
1139						Framework::window_close();
1140					}
1141					else
1142					{
1143						$msg = lang('Failed to delete account!');
1144						$msg_type = 'error';
1145					}
1146			}
1147		}
1148		// SMIME UPLOAD/DELETE/EXPORT control
1149		$content['hide_smime_upload'] = false;
1150		if (!empty($content['acc_smime_cred_id']))
1151		{
1152			if (!empty($content['smime_delete_p12']) &&
1153					Mail\Credentials::delete (
1154						$content['acc_id'],
1155						$content['called_for'] ? $content['called_for'] : $GLOBALS['egw_info']['user']['account_id'],
1156						Mail\Credentials::SMIME
1157				))
1158			{
1159				unset($content['acc_smime_password'], $content['smimeKeyUpload'], $content['smime_delete_p12'], $content['acc_smime_cred_id']);
1160				$content['hide_smime_upload'] = false;
1161			}
1162			else
1163			{
1164				// do NOT send smime private key to client side, it's unnecessary and binary blob breaks json encoding
1165				$content['acc_smime_password'] = Mail\Credentials::UNAVAILABLE;
1166
1167				$content['hide_smime_upload'] = true;
1168			}
1169		}
1170
1171		// disable delete button for new, not yet saved entries, if no delete rights or a non-standard identity selected
1172		$readonlys['button[delete]'] = empty($content['acc_id']) ||
1173			!Mail\Account::check_access(Acl::DELETE, $content) ||
1174			$content['ident_id'] != $content['std_ident_id'];
1175
1176		// if account is for multiple user, change delete confirmation to reflect that
1177		if (Mail\Account::is_multiple($content))
1178		{
1179			$tpl->setElementAttribute('button[delete]', 'onclick', "et2_dialog.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')");
1180		}
1181
1182		// if no edit access, make whole dialog readonly
1183		if (!$edit_access)
1184		{
1185			$readonlys['__ALL__'] = true;
1186			$readonlys['button[cancel]'] = false;
1187			// allow to edit notification-folders
1188			$readonlys['button[save]'] = $readonlys['button[apply]'] =
1189			$readonlys['notify_folders'] = $readonlys['notify_use_default'] = false;
1190			// allow to edit sMime stuff
1191			$readonlys['smimeGenerate'] = $readonlys['smimeKeyUpload'] = $readonlys['smime_pkcs12_password'] =
1192			$readonlys['smime_export_p12'] = $readonlys['smime_delete_p12'] = false;
1193		}
1194
1195		$sel_options['acc_imap_ssl'] = $sel_options['acc_sieve_ssl'] =
1196			$sel_options['acc_smtp_ssl'] = self::$ssl_types;
1197
1198		// admin access to account with no credentials available
1199		if ($this->is_admin && (empty($content['acc_imap_username']) || empty($content['acc_imap_host']) || $content['called_for']))
1200		{
1201			// cant connection to imap --> allow free entries in taglists
1202			foreach(array('acc_folder_sent', 'acc_folder_trash', 'acc_folder_draft', 'acc_folder_template', 'acc_folder_junk') as $folder)
1203			{
1204				$tpl->setElementAttribute($folder, 'allowFreeEntries', true);
1205			}
1206		}
1207		else
1208		{
1209			try {
1210				$sel_options['acc_folder_sent'] = $sel_options['acc_folder_trash'] =
1211					$sel_options['acc_folder_draft'] = $sel_options['acc_folder_template'] =
1212					$sel_options['acc_folder_junk'] = $sel_options['acc_folder_archive'] =
1213					$sel_options['notify_folders'] = $sel_options['acc_folder_ham'] =
1214						self::mailboxes(self::imap_client ($content));
1215				// Allow folder notification on INBOX for popup_only
1216				if ($GLOBALS['egw_info']['user']['preferences']['notifications']['notification_chain'] == 'popup_only')
1217				{
1218					$sel_options['notify_folders']['INBOX'] = lang('INBOX');
1219				}
1220			}
1221			catch(Exception $e) {
1222				if (self::$debug) _egw_log_exception($e);
1223				// let user know what the problem is and that he can fix it using wizard or deleting
1224				$msg = lang($e->getMessage())."\n\n".lang('You can use wizard to fix account settings or delete account.');
1225				$msg_type = 'error';
1226				// cant connection to imap --> allow free entries in taglists
1227				foreach(array('acc_folder_sent', 'acc_folder_trash', 'acc_folder_draft', 'acc_folder_template', 'acc_folder_junk') as $folder)
1228				{
1229					$tpl->setElementAttribute($folder, 'allowFreeEntries', true);
1230				}
1231			}
1232		}
1233
1234		$sel_options['acc_imap_type'] = Mail\Types::getIMAPServerTypes(false);
1235		$sel_options['acc_smtp_type'] = Mail\Types::getSMTPServerTypes(false);
1236		$sel_options['acc_imap_logintype'] = self::$login_types;
1237		$sel_options['ident_id'] = $content['identities'];
1238		$sel_options['acc_id'] = $content['accounts'];
1239		$sel_options['acc_further_identities'] = self::$further_identities;
1240
1241		// user is allowed to create or edit further identities
1242		if ($edit_access || $content['acc_further_identities'])
1243		{
1244			$sel_options['ident_id']['new'] = lang('Create new identity');
1245			$readonlys['ident_id'] = false;
1246
1247			// if no edit-access and identity is not standard identity --> allow to edit identity
1248			if (!$edit_access && $content['ident_id'] != $content['std_ident_id'])
1249			{
1250				$readonlys += array(
1251					'button[save]' => false, 'button[apply]' => false,
1252					'button[placeholders]' => false,
1253					'ident_name' => false,
1254					'ident_realname' => false, 'ident_email' => false, 'ident_email_alias' => false,
1255					'ident_org' => false, 'ident_signature' => false,
1256				);
1257			}
1258			if ($content['ident_id'] != $content['old_ident_id'] &&
1259				($content['old_ident_id'] || $content['ident_id'] != $content['std_ident_id']))
1260			{
1261				if ($content['ident_id'] > 0)
1262				{
1263					$identity = Mail\Account::read_identity($content['ident_id'], false, $content['called_for']);
1264					unset($identity['account_id']);
1265					$content = array_merge($content, $identity, array('ident_email_alias' => $identity['ident_email']));
1266				}
1267				else
1268				{
1269					$content['ident_name'] = $content['ident_realname'] = $content['ident_email'] =
1270						$content['ident_email_alias'] = $content['ident_org'] = $content['ident_signature'] = '';
1271				}
1272				if (empty($msg) && $edit_access && $content['ident_id'] && $content['ident_id'] != $content['std_ident_id'])
1273				{
1274					$msg = lang('Switch back to standard identity to save other account data.');
1275					$msg_type = 'help';
1276				}
1277				$content['old_ident_id'] = $content['ident_id'];
1278			}
1279		}
1280		$content['old_acc_id'] = $content['acc_id'];
1281
1282		// if only aliases are allowed for futher identities, add them as options
1283		// allow admins to always add arbitrary aliases
1284		if ($content['acc_further_identities'] == 2 && !$this->is_admin)
1285		{
1286			$sel_options['ident_email_alias'] = array_merge(
1287				array('' => $content['mailLocalAddress'].' ('.lang('Default').')'),
1288				array_combine($content['mailAlternateAddress'], $content['mailAlternateAddress']));
1289			// if admin explicitly set a non-alias, we need to add it to aliases to keep it after storing signature by user
1290			if ($content['ident_email'] !== $content['mailLocalAddress'] && !isset($sel_options['ident_email_alias'][$content['ident_email']]))
1291			{
1292				$sel_options['ident_email_alias'][$content['ident_email']] = $content['ident_email'];
1293			}
1294			// copy ident_email to select-box ident_email_alias, as et2 requires unique ids
1295			$content['ident_email_alias'] = $content['ident_email'];
1296			$content['select_ident_mail'] = true;
1297		}
1298
1299		// only allow to delete further identities, not a standard identity
1300		$readonlys['button[delete_identity]'] = !($content['ident_id'] > 0 && $content['ident_id'] != $content['std_ident_id']);
1301
1302		// disable aliases tab for default smtp class EGroupware\Api\Mail\Smtp
1303		$readonlys['tabs']['admin.mailaccount.aliases'] = !$content['acc_smtp_type'] ||
1304			$content['acc_smtp_type'] == 'EGroupware\\Api\\Mail\\Smtp';
1305		if ($readonlys['tabs']['admin.mailaccount.aliases'])
1306		{
1307			unset($sel_options['acc_further_identities'][2]);	// can limit identities to aliases without aliases ;-)
1308		}
1309
1310		// allow smtp class to disable certain features in alias tab
1311		if ($content['acc_smtp_type'] && class_exists($content['acc_smtp_type']) &&
1312			is_a($content['acc_smtp_type'], 'EGroupware\\Api\\Mail\\Smtp\\Ldap', true))
1313		{
1314			$content['no_forward_available'] = !constant($content['acc_smtp_type'].'::FORWARD_ATTR');
1315			if (!constant($content['acc_smtp_type'].'::FORWARD_ONLY_ATTR'))
1316			{
1317				$readonlys['deliveryMode'] = true;
1318			}
1319		}
1320
1321		// account allows users to change forwards
1322		if (!$edit_access && !$readonlys['tabs']['admin.mailaccount.aliases'] && $content['acc_user_forward'])
1323		{
1324			$readonlys['mailForwardingAddress'] = false;
1325		}
1326
1327		// allow imap classes to disable certain tabs or fields
1328		if (($class = Mail\Account::getIcClass($content['acc_imap_type'])) && class_exists($class) &&
1329			($imap_ro = call_user_func(array($class, 'getUIreadonlys'))))
1330		{
1331			$readonlys = array_merge($readonlys, $imap_ro, array(
1332				'tabs' => array_merge((array)$readonlys['tabs'], (array)$imap_ro['tabs']),
1333			));
1334		}
1335		Framework::message($msg ? $msg : (string)$_GET['msg'], $msg_type);
1336
1337		if (is_array($content['account_id']) && count($content['account_id']) > 1)
1338		{
1339			$tpl->setElementAttribute('account_id', 'multiple', true);
1340			$readonlys['button[multiple]'] = true;
1341		}
1342		// when called by admin for existing accounts, display further administrative actions
1343		if ($content['called_for'] && $content['acc_id'] > 0)
1344		{
1345			$admin_actions = array();
1346			foreach(Api\Hooks::process(array(
1347				'location' => 'emailadmin_edit',
1348				'account_id' => $content['called_for'],
1349				'acc_id' => $content['acc_id'],
1350			)) as $actions)
1351			{
1352				if ($actions) $admin_actions = array_merge($admin_actions, $actions);
1353			}
1354			if ($admin_actions) $tpl->setElementAttribute('admin_actions', 'actions', $admin_actions);
1355		}
1356		$content['admin_actions'] = (bool)$admin_actions;
1357
1358		//try to fix identities with no domain part set e.g. alias as identity
1359		if (!strpos($content['ident_email'], '@'))
1360		{
1361			$content['ident_email'] = Mail::fixInvalidAliasAddress (Api\Accounts::id2name($content['acc_imap_account_id'], 'account_email'), $content['ident_email']);
1362		}
1363
1364		// If no EPL available, show that in spamtitan blur
1365		$content['spamtitan_blur'] = $GLOBALS['egw_info']['user']['apps']['stylite'] ? '' : lang('SpamTitan integration requires EPL version');
1366
1367		$tpl->exec(static::APP_CLASS.'edit', $content, $sel_options, $readonlys, $content, 2);
1368	}
1369
1370	/**
1371	 * Saves the smime key
1372	 *
1373	 * @param array $content
1374	 * @param Etemplate $tpl
1375	 * @param int $account_id =null account to save smime key for, default current user
1376	 * @return int cred_id or null on error
1377	 */
1378	private static function save_smime_key(array $content, Etemplate $tpl, $account_id=null)
1379	{
1380		if (($pkcs12 = file_get_contents($content['smimeKeyUpload']['tmp_name'])))
1381		{
1382			$cert_info = Mail\Smime::extractCertPKCS12($pkcs12, $content['smime_pkcs12_password']);
1383			if (is_array($cert_info) && !empty($cert_info['cert']))
1384			{
1385				// save public key
1386				$smime = new Mail\Smime;
1387				$email = $smime->getEmailFromKey($cert_info['cert']);
1388				$AB_bo = new addressbook_bo();
1389				$AB_bo->set_smime_keys(array(
1390					$email => $cert_info['cert']
1391				));
1392				// save private key
1393				if (!isset($account_id)) $account_id = $GLOBALS['egw_info']['user']['account_id'];
1394				return Mail\Credentials::write($content['acc_id'], $email, $pkcs12, Mail\Credentials::SMIME, $account_id);
1395			}
1396			$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!'));
1397		}
1398		return null;
1399	}
1400
1401	/**
1402	 * Replace 0 with '' or back
1403	 *
1404	 * @param string|array &$account_id on return always array
1405	 * @param boolean $back =false
1406	 */
1407	private static function fix_account_id_0(&$account_id=null, $back=false)
1408	{
1409		if (!isset($account_id)) return;
1410
1411		if (!is_array($account_id))
1412		{
1413			$account_id = explode(',', $account_id);
1414		}
1415		if (($k = array_search($back?'':'0', $account_id)) !== false)
1416		{
1417			$account_id[$k] = $back ? '0' : '';
1418		}
1419	}
1420
1421	/**
1422	 * Instanciate imap-client
1423	 *
1424	 * @param array $content
1425	 * @param int $timeout =null default use value returned by Mail\Imap::getTimeOut()
1426	 * @return Horde_Imap_Client_Socket
1427	 */
1428	protected static function imap_client(array $content, $timeout=null)
1429	{
1430		return new Horde_Imap_Client_Socket(array(
1431			'username' => $content['acc_imap_username'],
1432			'password' => $content['acc_imap_password'],
1433			'hostspec' => $content['acc_imap_host'],
1434			'port' => $content['acc_imap_port'],
1435			'secure' => self::$ssl2secure[(string)array_search($content['acc_imap_ssl'], self::$ssl2type)],
1436			'timeout' => $timeout > 0 ? $timeout : Mail\Imap::getTimeOut(),
1437			'debug' => self::DEBUG_LOG,
1438		));
1439	}
1440
1441	/**
1442	 * Reorder SSL types to make sure we start with TLS, SSL, STARTTLS and insecure last
1443	 *
1444	 * @param array $data ssl => port pairs plus other data like value for 'username'
1445	 * @return array
1446	 */
1447	protected static function fix_ssl_order($data)
1448	{
1449		$ordered = array();
1450		foreach(array_merge(array('TLS', 'SSL', 'STARTTLS'), array_keys($data)) as $key)
1451		{
1452			if (array_key_exists($key, $data)) $ordered[$key] = $data[$key];
1453		}
1454		return $ordered;
1455	}
1456
1457	/**
1458	 * Query Mozilla's ISPDB
1459	 *
1460	 * Some providers eg. 1-and-1 do not report their hosted domains to ISPDB,
1461	 * therefore we try it with the found MX and it's domain-part (host-name removed).
1462	 *
1463	 * @param string $domain domain or email
1464	 * @param boolean $try_mx =true if domain itself is not found, try mx or domain-part (host removed) of mx
1465	 * @return array with values for keys 'displayName', 'imap', 'smtp', 'pop3', which each contain
1466	 *	array of arrays with values for keys 'hostname', 'port', 'socketType'=(SSL|STARTTLS), 'username'=%EMAILADDRESS%
1467	 */
1468	protected static function mozilla_ispdb($domain, $try_mx=true)
1469	{
1470		if (strpos($domain, '@') !== false) list(,$domain) = explode('@', $domain);
1471
1472		$url = 'https://autoconfig.thunderbird.net/v1.1/'.$domain;
1473		try {
1474			$xml = @simplexml_load_file($url);
1475			if (!$xml->emailProvider) throw new Api\Exception\NotFound();
1476			$provider = array(
1477				'displayName' => (string)$xml->emailProvider->displayName,
1478			);
1479			foreach($xml->emailProvider->children() as $tag => $server)
1480			{
1481				if (!in_array($tag, array('incomingServer', 'outgoingServer'))) continue;
1482				foreach($server->attributes() as $name => $value)
1483				{
1484					if ($name == 'type') $type = (string)$value;
1485				}
1486				$data = array();
1487				foreach($server as $name => $value)
1488				{
1489					foreach($value->children() as $tag => $val)
1490					{
1491						$data[$name][$tag] = (string)$val;
1492					}
1493					if (!isset($data[$name])) $data[$name] = (string)$value;
1494				}
1495				$provider[$type][] = $data;
1496			}
1497		}
1498		catch(Exception $e) {
1499			// ignore own not-found exception or xml parsing execptions
1500			unset($e);
1501
1502			if ($try_mx && ($dns = dns_get_record($domain, DNS_MX)))
1503			{
1504				$domain = $dns[0]['target'];
1505				if (!($provider = self::mozilla_ispdb($domain, false)))
1506				{
1507					list(,$domain) = explode('.', $domain, 2);
1508					$provider = self::mozilla_ispdb($domain, false);
1509				}
1510			}
1511			else
1512			{
1513				$provider = array();
1514			}
1515		}
1516		//error_log(__METHOD__."('$email') returning ".array2string($provider));
1517		return $provider;
1518	}
1519
1520	/**
1521	 * Guess possible server hostnames from email address:
1522	 *	- $type.$domain, mail.$domain
1523	 *  - replace host in MX with imap or mail
1524	 *  - MX for $domain
1525	 *
1526	 * @param string $email email address
1527	 * @param string $type ='imap' 'imap' or 'smtp', used as hostname beside 'mail'
1528	 * @return array of hostname => true pairs
1529	 */
1530	protected function guess_hosts($email, $type='imap')
1531	{
1532		list(,$domain) = explode('@', $email);
1533
1534		$hosts = array();
1535
1536		// try usuall names
1537		$hosts[$type.'.'.$domain] = true;
1538		$hosts['mail.'.$domain] = true;
1539		if ($type == 'smtp') $hosts['send.'.$domain] = true;
1540
1541		if (($dns = dns_get_record($domain, DNS_MX)))
1542		{
1543			//error_log(__METHOD__."('$email') dns_get_record('$domain', DNS_MX) returned ".array2string($dns));
1544			// hosts for office365 are outlook|smpt.office365.com for MX *.mail.protection.outlook.com
1545			if (substr($dns[0]['target'], -28) == '.mail.protection.outlook.com')
1546			{
1547				$hosts[($type == 'imap' ? 'outlook' : 'smtp').'.office365.com'] = true;
1548			}
1549			$hosts[preg_replace('/^[^.]+/', $type, $dns[0]['target'])] = true;
1550			$hosts[preg_replace('/^[^.]+/', 'mail', $dns[0]['target'])] = true;
1551			if ($type == 'smtp') $hosts[preg_replace('/^[^.]+/', 'send', $dns[0]['target'])] = true;
1552			$hosts[$dns[0]['target']] = true;
1553		}
1554
1555		// verify hosts in dns
1556		foreach(array_keys($hosts) as $host)
1557		{
1558			if (!dns_get_record($host, DNS_A)) unset($hosts[$host]);
1559		}
1560		//error_log(__METHOD__."('$email') returning ".array2string($hosts));
1561		return $hosts;
1562	}
1563
1564	/**
1565	 * Set mail account status wheter to 'active' or '' (inactive)
1566	 *
1567	 * @param array $_data account an array of data called via long task running dialog
1568	 *	$_data:array (
1569	 *		id => account_id,
1570	 *		qouta => quotaLimit,
1571	 *		domain => mailLocalAddress,
1572	 *		status => mail activation status('active'|'')
1573	 *	)
1574	 * @param string $etemplate_exec_id to check against CSRF
1575	 * @return json response
1576	 */
1577	public function ajax_activeAccounts($_data, $etemplate_exec_id)
1578	{
1579		Api\Etemplate\Request::csrfCheck($etemplate_exec_id, __METHOD__, func_get_args());
1580
1581		if (!$this->is_admin) die('no rights to be here!');
1582		$response = Api\Json\Response::get();
1583		if (($account = $GLOBALS['egw']->accounts->read($_data['id'])))
1584		{
1585			if ($_data['quota'] !== '' || $_data['accountStatus'] !== ''
1586				|| strpos($_data['domain'], '.'))
1587			{
1588				$emailadmin = Mail\Account::get_default();
1589				if (!Mail\Account::is_multiple($emailadmin))
1590				{
1591					$msg = lang('No default account found!');
1592					return $response->data($msg);
1593				}
1594
1595				$ea_account = Mail\Account::read($emailadmin->acc_id, $_data['id']);
1596				if (($userData = $ea_account->getUserData ()))
1597				{
1598					$userData = array(
1599						'acc_smtp_type' => $ea_account->acc_smtp_type,
1600						'accountStatus' => $_data['status'],
1601						'quotaLimit' => $_data['qouta']? $_data['qouta']: $userData['qoutaLimit'],
1602						'mailLocalAddress' => $userData['mailLocalAddress']
1603					);
1604
1605					if (strpos($_data['domain'], '.') !== false)
1606					{
1607						$userData['mailLocalAddress'] = preg_replace('/@'.preg_quote($ea_account->acc_domain).'$/', '@'.$_data['domain'], $userData['mailLocalAddress']);
1608
1609						foreach($userData['mailAlternateAddress'] as &$alias)
1610						{
1611							$alias = preg_replace('/@'.preg_quote($ea_account->acc_domain).'$/', '@'.$_data['domain'], $alias);
1612						}
1613					}
1614					// fullfill the saveUserData requirements
1615					$userData += $ea_account->params;
1616					$ea_account->saveUserData($_data['id'], $userData);
1617					$msg = '#'.$_data['id'].' '.$account['account_fullname']. ' '.($userData['accountStatus'] == 'active'? lang('activated'):lang('deactivated'));
1618				}
1619				else
1620				{
1621					$msg .= lang('No profile defined for user %1', '#'.$_data['id'].' '.$account['account_fullname']."\n");
1622
1623				}
1624			}
1625		}
1626		$response->data($msg);
1627	}
1628}
1629
1630/**
1631 * Trivial file logger, as Horde\ManageSieve does not support just a file
1632 */
1633class admin_mail_logger
1634{
1635	private $fp;
1636
1637	public function __construct($log)
1638	{
1639		$this->fp = is_resource($log) ? $log : fopen($log, 'a');
1640	}
1641
1642	public function debug($msg)
1643	{
1644		fwrite($this->fp, $msg."\n");
1645	}
1646}
1647