admin_mail   F
last analyzed

Complexity

Total Complexity 347

Size/Duplication

Total Lines 1595
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 789
dl 0
loc 1595
rs 1.811
c 0
b 0
f 0
wmc 347

15 Methods

Rating   Name   Duplication   Size   Complexity  
F smtp() 0 193 45
A fix_ssl_order() 0 8 3
A imap_client() 0 10 2
F sieve() 0 129 24
C mailboxes() 0 63 15
A save_smime_key() 0 21 5
C mozilla_ispdb() 0 50 14
A add() 0 24 5
B folder() 0 32 7
A fix_account_id_0() 0 11 6
F autoconfig() 0 141 33
B guess_hosts() 0 32 8
A __construct() 0 9 1
F edit() 0 544 167
C ajax_activeAccounts() 0 50 12

How to fix   Complexity   

Complex Class

Complex classes like admin_mail often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use admin_mail, and based on these observations, apply Extract Interface, too.

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 <[email protected]>
8
 * @copyright (c) 2013-18 by Ralf Becker <[email protected]>
9
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
10
 */
11
12
use EGroupware\Api;
13
use EGroupware\Api\Framework;
14
use EGroupware\Api\Acl;
15
use EGroupware\Api\Etemplate;
16
use EGroupware\Api\Mail;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Mail. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
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
 */
29
class 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;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$readonlys was never initialized. Although not strictly required by PHP, it is generally a good practice to add $readonlys = array(); before regardless.
Loading history...
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";
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with $ispdb['displayName']. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

244
			$content['output'] .= /** @scrutinizer ignore-call */ lang('Using data from Mozilla ISPDB for provider %1', $ispdb['displayName'])."\n";

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
245
			$hosts = array();
246
			foreach($ispdb['imap'] as $server)
0 ignored issues
show
Bug introduced by
The expression $ispdb['imap'] of type string is not traversable.
Loading history...
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)
0 ignored issues
show
introduced by
The condition $connected is always false.
Loading history...
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);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->add($content) targeting admin_mail::add() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
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);
0 ignored issues
show
Comprehensibility Best Practice introduced by
$sel_options was never initialized. Although not strictly required by PHP, it is generally a good practice to add $sel_options = array(); before regardless.
Loading history...
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();
0 ignored issues
show
Unused Code introduced by
The assignment to $all is dead and can be removed.
Loading history...
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;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$readonlys was never initialized. Although not strictly required by PHP, it is generally a good practice to add $readonlys = array(); before regardless.
Loading history...
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;
0 ignored issues
show
Unused Code introduced by
The assignment to $e is dead and can be removed.
Loading history...
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','');
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with 'Sieve'. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

558
						$content['sieve_output'] .= "\n"./** @scrutinizer ignore-call */ lang('Successful connected to %1 server%2.', 'Sieve','');

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
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;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$sel_options was never initialized. Although not strictly required by PHP, it is generally a good practice to add $sel_options = array(); before regardless.
Loading history...
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;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$readonlys was never initialized. Although not strictly required by PHP, it is generally a good practice to add $readonlys = array(); before regardless.
Loading history...
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";
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with $content['ispdb']['displayName']. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

669
				$content['smtp_output'] .= /** @scrutinizer ignore-call */ lang('Using data from Mozilla ISPDB for provider %1', $content['ispdb']['displayName'])."\n";

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
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;
0 ignored issues
show
Unused Code introduced by
The assignment to $e is dead and can be removed.
Loading history...
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'], '[email protected]', '');
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;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$sel_options was never initialized. Although not strictly required by PHP, it is generally a good practice to add $sel_options = array(); before regardless.
Loading history...
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();
0 ignored issues
show
introduced by
The condition is_array($content) is always false.
Loading history...
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']));
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->add(array('accoun...t)$_GET['account_id'])) targeting admin_mail::add() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
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);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->add($content) targeting admin_mail::add() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
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
0 ignored issues
show
Comprehensibility Best Practice introduced by
$readonlys was never initialized. Although not strictly required by PHP, it is generally a good practice to add $readonlys = array(); before regardless.
Loading history...
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);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->smtp($content) targeting admin_mail::smtp() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
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();
0 ignored issues
show
introduced by
$imap is of type EGroupware\Api\Mail\Imap, thus it always evaluated to true.
Loading history...
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();
0 ignored issues
show
Bug introduced by
The method retrieveRules() does not exist on EGroupware\Api\Mail\Imap. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1006
								$account->imapServer()->/** @scrutinizer ignore-call */ retrieveRules();
Loading history...
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')));
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with lang('Forward only'). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1015
								throw new Api\Exception\WrongUserinput(/** @scrutinizer ignore-call */ lang('You need to specify a forwarding address, when checking "%1"!', lang('Forward only')));

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
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;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$sel_options was never initialized. Although not strictly required by PHP, it is generally a good practice to add $sel_options = array(); before regardless.
Loading history...
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
		$tpl->exec(static::APP_CLASS.'edit', $content, $sel_options, $readonlys, $content, 2);
1365
	}
1366
1367
	/**
1368
	 * Saves the smime key
1369
	 *
1370
	 * @param array $content
1371
	 * @param Etemplate $tpl
1372
	 * @param int $account_id =null account to save smime key for, default current user
1373
	 * @return int cred_id or null on error
1374
	 */
1375
	private static function save_smime_key(array $content, Etemplate $tpl, $account_id=null)
1376
	{
1377
		if (($pkcs12 = file_get_contents($content['smimeKeyUpload']['tmp_name'])))
1378
		{
1379
			$cert_info = Mail\Smime::extractCertPKCS12($pkcs12, $content['smime_pkcs12_password']);
1380
			if (is_array($cert_info) && !empty($cert_info['cert']))
0 ignored issues
show
introduced by
The condition is_array($cert_info) is always false.
Loading history...
1381
			{
1382
				// save public key
1383
				$smime = new Mail\Smime;
1384
				$email = $smime->getEmailFromKey($cert_info['cert']);
1385
				$AB_bo = new addressbook_bo();
1386
				$AB_bo->set_smime_keys(array(
1387
					$email => $cert_info['cert']
1388
				));
1389
				// save private key
1390
				if (!isset($account_id)) $account_id = $GLOBALS['egw_info']['user']['account_id'];
1391
				return Mail\Credentials::write($content['acc_id'], $email, $pkcs12, Mail\Credentials::SMIME, $account_id);
1392
			}
1393
			$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!'));
1394
		}
1395
		return null;
1396
	}
1397
1398
	/**
1399
	 * Replace 0 with '' or back
1400
	 *
1401
	 * @param string|array &$account_id on return always array
1402
	 * @param boolean $back =false
1403
	 */
1404
	private static function fix_account_id_0(&$account_id=null, $back=false)
1405
	{
1406
		if (!isset($account_id)) return;
1407
1408
		if (!is_array($account_id))
1409
		{
1410
			$account_id = explode(',', $account_id);
1411
		}
1412
		if (($k = array_search($back?'':'0', $account_id)) !== false)
1413
		{
1414
			$account_id[$k] = $back ? '0' : '';
1415
		}
1416
	}
1417
1418
	/**
1419
	 * Instanciate imap-client
1420
	 *
1421
	 * @param array $content
1422
	 * @param int $timeout =null default use value returned by Mail\Imap::getTimeOut()
1423
	 * @return Horde_Imap_Client_Socket
1424
	 */
1425
	protected static function imap_client(array $content, $timeout=null)
1426
	{
1427
		return new Horde_Imap_Client_Socket(array(
1428
			'username' => $content['acc_imap_username'],
1429
			'password' => $content['acc_imap_password'],
1430
			'hostspec' => $content['acc_imap_host'],
1431
			'port' => $content['acc_imap_port'],
1432
			'secure' => self::$ssl2secure[(string)array_search($content['acc_imap_ssl'], self::$ssl2type)],
1433
			'timeout' => $timeout > 0 ? $timeout : Mail\Imap::getTimeOut(),
1434
			'debug' => self::DEBUG_LOG,
1435
		));
1436
	}
1437
1438
	/**
1439
	 * Reorder SSL types to make sure we start with TLS, SSL, STARTTLS and insecure last
1440
	 *
1441
	 * @param array $data ssl => port pairs plus other data like value for 'username'
1442
	 * @return array
1443
	 */
1444
	protected static function fix_ssl_order($data)
1445
	{
1446
		$ordered = array();
1447
		foreach(array_merge(array('TLS', 'SSL', 'STARTTLS'), array_keys($data)) as $key)
1448
		{
1449
			if (array_key_exists($key, $data)) $ordered[$key] = $data[$key];
1450
		}
1451
		return $ordered;
1452
	}
1453
1454
	/**
1455
	 * Query Mozilla's ISPDB
1456
	 *
1457
	 * Some providers eg. 1-and-1 do not report their hosted domains to ISPDB,
1458
	 * therefore we try it with the found MX and it's domain-part (host-name removed).
1459
	 *
1460
	 * @param string $domain domain or email
1461
	 * @param boolean $try_mx =true if domain itself is not found, try mx or domain-part (host removed) of mx
1462
	 * @return array with values for keys 'displayName', 'imap', 'smtp', 'pop3', which each contain
1463
	 *	array of arrays with values for keys 'hostname', 'port', 'socketType'=(SSL|STARTTLS), 'username'=%EMAILADDRESS%
1464
	 */
1465
	protected static function mozilla_ispdb($domain, $try_mx=true)
1466
	{
1467
		if (strpos($domain, '@') !== false) list(,$domain) = explode('@', $domain);
1468
1469
		$url = 'https://autoconfig.thunderbird.net/v1.1/'.$domain;
1470
		try {
1471
			$xml = @simplexml_load_file($url);
1472
			if (!$xml->emailProvider) throw new Api\Exception\NotFound();
1473
			$provider = array(
1474
				'displayName' => (string)$xml->emailProvider->displayName,
1475
			);
1476
			foreach($xml->emailProvider->children() as $tag => $server)
1477
			{
1478
				if (!in_array($tag, array('incomingServer', 'outgoingServer'))) continue;
1479
				foreach($server->attributes() as $name => $value)
1480
				{
1481
					if ($name == 'type') $type = (string)$value;
1482
				}
1483
				$data = array();
1484
				foreach($server as $name => $value)
1485
				{
1486
					foreach($value->children() as $tag => $val)
0 ignored issues
show
Comprehensibility Bug introduced by
$tag is overwriting a variable from outer foreach loop.
Loading history...
1487
					{
1488
						$data[$name][$tag] = (string)$val;
1489
					}
1490
					if (!isset($data[$name])) $data[$name] = (string)$value;
1491
				}
1492
				$provider[$type][] = $data;
1493
			}
1494
		}
1495
		catch(Exception $e) {
1496
			// ignore own not-found exception or xml parsing execptions
1497
			unset($e);
1498
1499
			if ($try_mx && ($dns = dns_get_record($domain, DNS_MX)))
1500
			{
1501
				$domain = $dns[0]['target'];
1502
				if (!($provider = self::mozilla_ispdb($domain, false)))
1503
				{
1504
					list(,$domain) = explode('.', $domain, 2);
1505
					$provider = self::mozilla_ispdb($domain, false);
1506
				}
1507
			}
1508
			else
1509
			{
1510
				$provider = array();
1511
			}
1512
		}
1513
		//error_log(__METHOD__."('$email') returning ".array2string($provider));
1514
		return $provider;
1515
	}
1516
1517
	/**
1518
	 * Guess possible server hostnames from email address:
1519
	 *	- $type.$domain, mail.$domain
1520
	 *  - replace host in MX with imap or mail
1521
	 *  - MX for $domain
1522
	 *
1523
	 * @param string $email email address
1524
	 * @param string $type ='imap' 'imap' or 'smtp', used as hostname beside 'mail'
1525
	 * @return array of hostname => true pairs
1526
	 */
1527
	protected function guess_hosts($email, $type='imap')
1528
	{
1529
		list(,$domain) = explode('@', $email);
1530
1531
		$hosts = array();
1532
1533
		// try usuall names
1534
		$hosts[$type.'.'.$domain] = true;
1535
		$hosts['mail.'.$domain] = true;
1536
		if ($type == 'smtp') $hosts['send.'.$domain] = true;
1537
1538
		if (($dns = dns_get_record($domain, DNS_MX)))
1539
		{
1540
			//error_log(__METHOD__."('$email') dns_get_record('$domain', DNS_MX) returned ".array2string($dns));
1541
			// hosts for office365 are outlook|smpt.office365.com for MX *.mail.protection.outlook.com
1542
			if (substr($dns[0]['target'], -28) == '.mail.protection.outlook.com')
1543
			{
1544
				$hosts[($type == 'imap' ? 'outlook' : 'smtp').'.office365.com'] = true;
1545
			}
1546
			$hosts[preg_replace('/^[^.]+/', $type, $dns[0]['target'])] = true;
1547
			$hosts[preg_replace('/^[^.]+/', 'mail', $dns[0]['target'])] = true;
1548
			if ($type == 'smtp') $hosts[preg_replace('/^[^.]+/', 'send', $dns[0]['target'])] = true;
1549
			$hosts[$dns[0]['target']] = true;
1550
		}
1551
1552
		// verify hosts in dns
1553
		foreach(array_keys($hosts) as $host)
1554
		{
1555
			if (!dns_get_record($host, DNS_A)) unset($hosts[$host]);
1556
		}
1557
		//error_log(__METHOD__."('$email') returning ".array2string($hosts));
1558
		return $hosts;
1559
	}
1560
1561
	/**
1562
	 * Set mail account status wheter to 'active' or '' (inactive)
1563
	 *
1564
	 * @param array $_data account an array of data called via long task running dialog
1565
	 *	$_data:array (
1566
	 *		id => account_id,
1567
	 *		qouta => quotaLimit,
1568
	 *		domain => mailLocalAddress,
1569
	 *		status => mail activation status('active'|'')
1570
	 *	)
1571
	 * @param string $etemplate_exec_id to check against CSRF
1572
	 * @return json response
1573
	 */
1574
	public function ajax_activeAccounts($_data, $etemplate_exec_id)
1575
	{
1576
		Api\Etemplate\Request::csrfCheck($etemplate_exec_id, __METHOD__, func_get_args());
1577
1578
		if (!$this->is_admin) die('no rights to be here!');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
1579
		$response = Api\Json\Response::get();
1580
		if (($account = $GLOBALS['egw']->accounts->read($_data['id'])))
1581
		{
1582
			if ($_data['quota'] !== '' || $_data['accountStatus'] !== ''
1583
				|| strpos($_data['domain'], '.'))
1584
			{
1585
				$emailadmin = Mail\Account::get_default();
1586
				if (!Mail\Account::is_multiple($emailadmin))
1587
				{
1588
					$msg = lang('No default account found!');
1589
					return $response->data($msg);
1590
				}
1591
1592
				$ea_account = Mail\Account::read($emailadmin->acc_id, $_data['id']);
1593
				if (($userData = $ea_account->getUserData ()))
1594
				{
1595
					$userData = array(
1596
						'acc_smtp_type' => $ea_account->acc_smtp_type,
1597
						'accountStatus' => $_data['status'],
1598
						'quotaLimit' => $_data['qouta']? $_data['qouta']: $userData['qoutaLimit'],
1599
						'mailLocalAddress' => $userData['mailLocalAddress']
1600
					);
1601
1602
					if (strpos($_data['domain'], '.') !== false)
1603
					{
1604
						$userData['mailLocalAddress'] = preg_replace('/@'.preg_quote($ea_account->acc_domain).'$/', '@'.$_data['domain'], $userData['mailLocalAddress']);
1605
1606
						foreach($userData['mailAlternateAddress'] as &$alias)
1607
						{
1608
							$alias = preg_replace('/@'.preg_quote($ea_account->acc_domain).'$/', '@'.$_data['domain'], $alias);
1609
						}
1610
					}
1611
					// fullfill the saveUserData requirements
1612
					$userData += $ea_account->params;
1613
					$ea_account->saveUserData($_data['id'], $userData);
1614
					$msg = '#'.$_data['id'].' '.$account['account_fullname']. ' '.($userData['accountStatus'] == 'active'? lang('activated'):lang('deactivated'));
1615
				}
1616
				else
1617
				{
1618
					$msg .= lang('No profile defined for user %1', '#'.$_data['id'].' '.$account['account_fullname']."\n");
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with '#' . $_data['id'] . ' '...ccount_fullname'] . ' '. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1618
					$msg .= /** @scrutinizer ignore-call */ lang('No profile defined for user %1', '#'.$_data['id'].' '.$account['account_fullname']."\n");

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Comprehensibility Best Practice introduced by
The variable $msg seems to be never defined.
Loading history...
1619
1620
				}
1621
			}
1622
		}
1623
		$response->data($msg);
1624
	}
1625
}
1626
1627
/**
1628
 * Trivial file logger, as Horde\ManageSieve does not support just a file
1629
 */
1630
class admin_mail_logger
1631
{
1632
	private $fp;
1633
1634
	public function __construct($log)
1635
	{
1636
		$this->fp = is_resource($log) ? $log : fopen($log, 'a');
1637
	}
1638
1639
	public function debug($msg)
1640
	{
1641
		fwrite($this->fp, $msg."\n");
1642
	}
1643
}
1644