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