2016-03-28 20:51:38 +02:00
< ? php
/**
2018-04-10 13:15:09 +02:00
* EGroupware EMailAdmin : Wizard to create mail accounts
2016-03-28 20:51:38 +02:00
*
2018-04-10 13:15:09 +02:00
* @ link http :// www . egroupware . org
2016-03-28 20:51:38 +02:00
* @ package emailadmin
2018-04-10 13:15:09 +02:00
* @ author Ralf Becker < rb @ egroupware . org >
* @ copyright ( c ) 2013 - 18 by Ralf Becker < rb @ egroupware . org >
2016-03-28 20:51:38 +02:00
* @ license http :// opensource . org / licenses / gpl - license . php GPL - GNU General Public License
*/
use EGroupware\Api ;
2016-04-27 21:12:20 +02:00
use EGroupware\Api\Framework ;
use EGroupware\Api\Acl ;
use EGroupware\Api\Etemplate ;
2016-03-28 20:51:38 +02:00
use EGroupware\Api\Mail ;
2022-12-23 21:32:54 +01:00
use EGroupware\Api\Auth\OpenIDConnectClient ;
use Jumbojett\OpenIDConnectClientException ;
2016-03-28 20:51:38 +02:00
/**
* Wizard to create mail accounts
*
* Wizard uses follow heuristic to search for IMAP accounts :
* 1. query Mozilla ISPDB for domain from email ( perfering SSL over STARTTLS over insecure connection )
* 2. guessing and verifying in DNS server - names based on domain from email :
* - ( imap | smtp ) . $domain , mail . $domain
* - MX is *. mail . protection . outlook . com use ( outlook | smtp ) . office365 . com
* - MX for $domain
* - replace host in MX with ( imap | smtp ) or mail
*/
class admin_mail
{
/**
* Enable logging of IMAP communication to given path , eg . / tmp / autoconfig . log
*/
2023-01-25 13:38:40 +01:00
const DEBUG_LOG = null ; //'/var/lib/egroupware/imap.log';
2016-03-28 20:51:38 +02:00
/**
* Connection timeout in seconds used in autoconfig , can and should be really short !
*/
const TIMEOUT = 3 ;
/**
* Prefix for callback names
*
* Used as static :: APP_CLASS in etemplate :: exec (), to allow mail app extending this class .
*/
const APP_CLASS = 'admin.admin_mail.' ;
/**
* 0 : No SSL
*/
const SSL_NONE = Mail\Account :: SSL_NONE ;
/**
* 1 : STARTTLS on regular tcp connection / port
*/
const SSL_STARTTLS = Mail\Account :: SSL_STARTTLS ;
/**
* 3 : SSL ( inferior to TLS ! )
*/
const SSL_SSL = Mail\Account :: SSL_SSL ;
/**
* 2 : require TLS version 1 + , no SSL version 2 or 3
*/
const SSL_TLS = Mail\Account :: SSL_TLS ;
/**
* 8 : if set , verify certifcate ( currently not implemented in Horde_Imap_Client ! )
*/
const SSL_VERIFY = Mail\Account :: SSL_VERIFY ;
/**
* Log exception including trace to error - log , instead of just displaying the message .
*
* @ var boolean
*/
public static $debug = false ;
/**
* Methods callable via menuaction
*
* @ var array
*/
public $public_functions = array (
'add' => true ,
'edit' => true ,
'ajax_activeAccounts' => true
);
/**
* Supported ssl types including none
*
* @ var array
*/
public static $ssl_types = array (
self :: SSL_TLS => 'TLS' , // SSL with minimum TLS (no SSL v.2 or v.3), requires Horde_Imap_Client-2.16.0/Horde_Socket_Client-1.1.0
self :: SSL_SSL => 'SSL' ,
self :: SSL_STARTTLS => 'STARTTLS' ,
'no' => 'no' ,
);
/**
* Convert ssl - type to Horde secure parameter
*
* @ var array
*/
public static $ssl2secure = array (
'SSL' => 'ssl' ,
'STARTTLS' => 'tls' ,
'TLS' => 'tlsv1' , // SSL with minimum TLS (no SSL v.2 or v.3), requires Horde_Imap_Client-2.16.0/Horde_Socket_Client-1.1.0
);
/**
* Convert ssl - type to eMailAdmin acc_ ( imap | sieve | smtp ) _ssl integer value
*
* @ var array
*/
public static $ssl2type = array (
'TLS' => self :: SSL_TLS ,
'SSL' => self :: SSL_SSL ,
'STARTTLS' => self :: SSL_STARTTLS ,
'no' => self :: SSL_NONE ,
);
/**
* Available IMAP login types
*
* @ var array
*/
public static $login_types = array (
'' => 'Username specified below for all' ,
'standard' => 'username from account' ,
'vmailmgr' => 'username@domainname' ,
//'admin' => 'Username/Password defined by admin',
'uidNumber' => 'UserId@domain eg. u1234@domain' ,
'email' => 'EMail-address from account' ,
2023-12-15 16:12:47 +01:00
'domain/username' => 'Exchange: domain/username' ,
2016-03-28 20:51:38 +02:00
);
2016-10-28 14:27:07 +02:00
/**
* Options for further identities
*
* @ var array
*/
public static $further_identities = array (
0 => 'Forbid users to create identities' ,
1 => 'Allow users to create further identities' ,
2 => 'Allow users to create identities for aliases' ,
);
2016-03-28 20:51:38 +02:00
/**
* List of domains know to not support Sieve
*
2022-12-24 22:39:11 +01:00
* Used to switch Sieve off by default , thought users can always try switching it on .
2016-03-28 20:51:38 +02:00
* Testing not existing Sieve with google takes a long time , as ports are open ,
* but not answering ...
*
* @ var array
*/
public static $no_sieve_blacklist = array ( 'gmail.com' , 'googlemail.com' , 'outlook.office365.com' );
/**
* Is current use a mail administrator / has run rights for EMailAdmin
*
* @ var boolean
*/
protected $is_admin = false ;
/**
* Constructor
*/
public function __construct ()
{
$this -> is_admin = isset ( $GLOBALS [ 'egw_info' ][ 'user' ][ 'apps' ][ 'admin' ]);
// for some reason most translation for account-wizard are in mail
Api\Translation :: add_app ( 'mail' );
// Horde use locale for translation of error messages
Api\Preferences :: setlocale ( LC_MESSAGES );
}
/**
* Step 1 : IMAP account
*
* @ param array $content
2021-11-27 10:21:54 +01:00
* @ param string $msg
2016-03-28 20:51:38 +02:00
*/
public function add ( array $content = array (), $msg = '' , $msg_type = 'success' )
{
2016-04-27 21:12:20 +02:00
$tpl = new Etemplate ( 'admin.mailwizard' );
2016-03-28 20:51:38 +02:00
if ( empty ( $content [ 'account_id' ]))
{
$content [ 'account_id' ] = $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ];
}
// add some defaults if not already set (+= does not overwrite existing values!)
$content += array (
'ident_realname' => $GLOBALS [ 'egw' ] -> accounts -> id2name ( $content [ 'account_id' ], 'account_fullname' ),
'ident_email' => $GLOBALS [ 'egw' ] -> accounts -> id2name ( $content [ 'account_id' ], 'account_email' ),
'acc_imap_port' => 993 ,
'manual_class' => 'emailadmin_manual' ,
);
2016-04-27 21:12:20 +02:00
Framework :: message ( $msg ? $msg : ( string ) $_GET [ 'msg' ], $msg_type );
2016-03-28 20:51:38 +02:00
if ( ! empty ( $content [ 'acc_imap_host' ]) || ! empty ( $content [ 'acc_imap_username' ]))
{
$readonlys [ 'button[manual]' ] = true ;
unset ( $content [ 'manual_class' ]);
}
$tpl -> exec ( static :: APP_CLASS . 'autoconfig' , $content , array (
'acc_imap_ssl' => self :: $ssl_types ,
), $readonlys , $content , 2 );
}
/**
* Try to autoconfig an account
*
* @ param array $content
*/
public function autoconfig ( array $content )
{
// user pressed [Skip IMAP] --> jump to SMTP config
2021-10-06 16:07:47 +02:00
if ( ! empty ( $content [ 'button' ]) && key ( $content [ 'button' ]) === 'skip_imap' )
2016-03-28 20:51:38 +02:00
{
unset ( $content [ 'button' ]);
if ( ! isset ( $content [ 'acc_smtp_host' ])) $content [ 'acc_smtp_host' ] = '' ; // do manual mode right away
return $this -> smtp ( $content , lang ( 'Skipping IMAP configuration!' ));
}
2022-12-23 21:32:54 +01:00
$tpl = new Etemplate ( 'admin.mailwizard' );
$sel_options = $readonlys = $hosts = [];
2016-03-28 20:51:38 +02:00
2022-12-23 21:32:54 +01:00
$connected = $content [ 'connected' ] ? ? null ;
2016-03-28 20:51:38 +02:00
if ( empty ( $content [ 'acc_imap_username' ]))
{
$content [ 'acc_imap_username' ] = $content [ 'ident_email' ];
}
2023-01-23 21:11:21 +01:00
// supported oauth provider or mail-server of them for custom domains
if (( $oauth = OpenIDConnectClient :: providerByDomain ( $content [ 'acc_imap_username' ], $content [ 'acc_imap_host' ])))
2022-12-23 21:32:54 +01:00
{
2022-12-25 22:19:14 +01:00
$content [ 'output' ] .= lang ( 'Using IMAP:%1, SMTP:%2, OAUTH:%3:' , $oauth [ 'imap' ], $oauth [ 'smtp' ], $oauth [ 'provider' ]) . " \n " ;
2022-12-24 22:39:11 +01:00
$hosts [ $oauth [ 'imap' ]] = true ;
2023-01-13 02:33:31 +01:00
$content += self :: oauth2content ( $oauth );
2022-12-25 21:49:37 +01:00
}
elseif ( ! empty ( $content [ 'acc_imap_host' ]))
{
$hosts = array ( $content [ 'acc_imap_host' ] => true );
if ( $content [ 'acc_imap_port' ] > 0 && ! in_array ( $content [ 'acc_imap_port' ], array ( 143 , 993 )))
{
$ssl_type = ( string ) array_search ( $content [ 'acc_imap_ssl' ], self :: $ssl2type );
if ( $ssl_type === '' ) $ssl_type = 'insecure' ;
$hosts [ $content [ 'acc_imap_host' ]] = array (
$ssl_type => $content [ 'acc_imap_port' ],
);
}
2022-12-23 21:32:54 +01:00
}
2016-03-28 20:51:38 +02:00
elseif (( $ispdb = self :: mozilla_ispdb ( $content [ 'ident_email' ])) && count ( $ispdb [ 'imap' ]))
{
$content [ 'ispdb' ] = $ispdb ;
$content [ 'output' ] .= lang ( 'Using data from Mozilla ISPDB for provider %1' , $ispdb [ 'displayName' ]) . " \n " ;
$hosts = array ();
foreach ( $ispdb [ 'imap' ] as $server )
{
if ( ! isset ( $hosts [ $server [ 'hostname' ]]))
{
$hosts [ $server [ 'hostname' ]] = array ( 'username' => $server [ 'username' ]);
}
if ( strtoupper ( $server [ 'socketType' ]) == 'SSL' ) // try TLS first
{
$hosts [ $server [ 'hostname' ]][ 'TLS' ] = $server [ 'port' ];
}
$hosts [ $server [ 'hostname' ]][ strtoupper ( $server [ 'socketType' ])] = $server [ 'port' ];
// make sure we prefer SSL over STARTTLS over insecure
if ( count ( $hosts [ $server [ 'hostname' ]]) > 2 )
{
$hosts [ $server [ 'hostname' ]] = self :: fix_ssl_order ( $hosts [ $server [ 'hostname' ]]);
}
}
}
else
{
$hosts = $this -> guess_hosts ( $content [ 'ident_email' ], 'imap' );
}
2022-12-24 22:39:11 +01:00
// check if support OAuth for that domain or we have a password
2022-12-25 21:49:37 +01:00
if ( empty ( $oauth ) && empty ( $content [ 'acc_oauth_provider_url' ]) && empty ( $content [ 'acc_imap_password' ]))
2022-12-24 22:39:11 +01:00
{
Etemplate :: set_validation_error ( 'acc_imap_password' , lang ( 'Field must not be empty!' ));
$connected = false ;
}
2016-03-28 20:51:38 +02:00
// iterate over all hosts and try to connect
2022-12-23 21:32:54 +01:00
foreach ( ! isset ( $connected ) ? $hosts : [] as $host => $data )
2016-03-28 20:51:38 +02:00
{
2023-01-13 02:33:31 +01:00
// check if we support OAuth for the (manual) configured mail-server
if ( empty ( $content [ 'acc_oauth_provider_url' ]) && ( $oauth = OpenIDConnectClient :: providerByDomain ( $content [ 'acc_imap_username' ], $host )))
{
$content += self :: oauth2content ( $oauth );
}
2016-03-28 20:51:38 +02:00
$content [ 'acc_imap_host' ] = $host ;
// by default we check SSL, STARTTLS and at last an insecure connection
if ( ! is_array ( $data )) $data = array ( 'TLS' => 993 , 'SSL' => 993 , 'STARTTLS' => 143 , 'insecure' => 143 );
foreach ( $data as $ssl => $port )
{
if ( $ssl === 'username' ) continue ;
$content [ 'acc_imap_ssl' ] = ( int ) self :: $ssl2type [ $ssl ];
$e = null ;
try {
$content [ 'output' ] .= " \n " . Api\DateTime :: to ( 'now' , 'H:i:s' ) . " : Trying $ssl connection to $host : $port ... \n " ;
$content [ 'acc_imap_port' ] = $port ;
$imap = self :: imap_client ( $content , self :: TIMEOUT );
//$content['output'] .= array2string($imap->capability());
$imap -> login ();
$content [ 'output' ] .= " \n " . lang ( 'Successful connected to %1 server%2.' , 'IMAP' , ' ' . lang ( 'and logged in' )) . " \n " ;
if ( ! $imap -> isSecureConnection ())
{
$content [ 'output' ] .= lang ( 'Connection is NOT secure! Everyone can read eg. your credentials.' ) . " \n " ;
$content [ 'acc_imap_ssl' ] = 'no' ;
}
//$content['output'] .= "\n\n".array2string($imap->capability());
$content [ 'connected' ] = $connected = true ;
break 2 ;
}
catch ( Horde_Imap_Client_Exception $e )
{
switch ( $e -> getCode ())
{
case Horde_Imap_Client_Exception :: LOGIN_AUTHENTICATIONFAILED :
$content [ 'output' ] .= " \n " . $e -> getMessage () . " \n " ;
break 3 ; // no need to try other SSL or non-SSL connections, if auth failed
case Horde_Imap_Client_Exception :: SERVER_CONNECT :
$content [ 'output' ] .= " \n " . $e -> getMessage () . " \n " ;
if ( $ssl == 'STARTTLS' ) break 2 ; // no need to try insecure connection on same port
break ;
default :
$content [ 'output' ] .= " \n " . get_class ( $e ) . ': ' . $e -> getMessage () . ' (' . $e -> getCode () . ')' . " \n " ;
//$content['output'] .= $e->getTraceAsString()."\n";
}
if ( self :: $debug ) _egw_log_exception ( $e );
}
catch ( Exception $e ) {
$content [ 'output' ] .= " \n " . get_class ( $e ) . ': ' . $e -> getMessage () . ' (' . $e -> getCode () . ')' . " \n " ;
//$content['output'] .= $e->getTraceAsString()."\n";
if ( self :: $debug ) _egw_log_exception ( $e );
}
}
}
if ( $connected ) // continue with next wizard step: define folders
{
unset ( $content [ 'button' ]);
return $this -> folder ( $content , lang ( 'Successful connected to %1 server%2.' , 'IMAP' , ' ' . lang ( 'and logged in' )) .
( $imap -> isSecureConnection () ? '' : " \n " . lang ( 'Connection is NOT secure! Everyone can read eg. your credentials.' )));
}
// add validation error, if we can identify a field
if ( ! $connected && $e instanceof Horde_Imap_Client_Exception )
{
switch ( $e -> getCode ())
{
case Horde_Imap_Client_Exception :: LOGIN_AUTHENTICATIONFAILED :
2016-04-27 21:12:20 +02:00
Etemplate :: set_validation_error ( 'acc_imap_username' , lang ( $e -> getMessage ()));
Etemplate :: set_validation_error ( 'acc_imap_password' , lang ( $e -> getMessage ()));
2016-03-28 20:51:38 +02:00
break ;
case Horde_Imap_Client_Exception :: SERVER_CONNECT :
2016-04-27 21:12:20 +02:00
Etemplate :: set_validation_error ( 'acc_imap_host' , lang ( $e -> getMessage ()));
2016-03-28 20:51:38 +02:00
break ;
}
}
$readonlys [ 'button[manual]' ] = true ;
unset ( $content [ 'manual_class' ]);
$sel_options [ 'acc_imap_ssl' ] = self :: $ssl_types ;
2022-12-23 21:32:54 +01:00
$tpl -> exec ( static :: APP_CLASS . 'autoconfig' , $content , $sel_options , $readonlys ,
array_diff_key ( $content , [ 'output' => true ]), 2 );
2016-03-28 20:51:38 +02:00
}
2023-01-13 02:33:31 +01:00
/**
* Convert OAuth provider data to our content - names
*
* @ param array $oauth
* @ return array
*/
protected static function oauth2content ( array $oauth )
{
return [
'acc_smpt_host' => $oauth [ 'smtp' ],
'acc_sieve_enabled' => false ,
'acc_oauth_provider_url' => $oauth [ 'provider' ],
'acc_oauth_client_id' => $oauth [ 'client' ],
'acc_oauth_client_secret' => $oauth [ 'secret' ],
'acc_oauth_scopes' => $oauth [ 'scopes' ],
OpenIDConnectClient :: ADD_CLIENT_TO_WELL_KNOWN => $oauth [ OpenIDConnectClient :: ADD_CLIENT_TO_WELL_KNOWN ] ? ? null ,
OpenIDConnectClient :: ADD_AUTH_PARAM => $oauth [ OpenIDConnectClient :: ADD_AUTH_PARAM ] ? ? null ,
];
}
2016-03-28 20:51:38 +02:00
/**
* Step 2 : Folder - let user select trash , sent , drafs and template folder
*
* @ param array $content
* @ param string $msg = ''
* @ param Horde_Imap_Client_Socket $imap = null
*/
public function folder ( array $content , $msg = '' , Horde_Imap_Client_Socket $imap = null )
{
2021-10-06 16:07:47 +02:00
if ( ! empty ( $content [ 'button' ]))
2016-03-28 20:51:38 +02:00
{
2019-02-12 22:13:45 +01:00
$button = key ( $content [ 'button' ]);
2016-03-28 20:51:38 +02:00
unset ( $content [ 'button' ]);
switch ( $button )
{
case 'back' :
return $this -> add ( $content );
case 'continue' :
return $this -> sieve ( $content );
}
}
$content [ 'msg' ] = $msg ;
if ( ! isset ( $imap )) $imap = self :: imap_client ( $content );
try {
//_debug_array($content);
$sel_options [ 'acc_folder_sent' ] = $sel_options [ 'acc_folder_trash' ] =
$sel_options [ 'acc_folder_draft' ] = $sel_options [ 'acc_folder_template' ] =
2017-05-31 15:10:58 +02:00
$sel_options [ 'acc_folder_junk' ] = $sel_options [ 'acc_folder_archive' ] =
$sel_options [ 'acc_folder_ham' ] = self :: mailboxes ( $imap , $content );
2016-03-28 20:51:38 +02:00
}
catch ( Exception $e ) {
$content [ 'msg' ] = $e -> getMessage ();
if ( self :: $debug ) _egw_log_exception ( $e );
}
2016-04-27 21:12:20 +02:00
$tpl = new Etemplate ( 'admin.mailwizard.folder' );
2023-03-08 11:08:06 +01:00
$tpl -> exec ( static :: APP_CLASS . 'folder' , $content , $sel_options , array (), $content , 2 );
2016-03-28 20:51:38 +02:00
}
/**
* Query mailboxes and ( optional ) detect special folders
*
* @ param Horde_Imap_Client_Socket $imap
* @ param array & $content = null on return values for acc_folder_ ( sent | trash | draft | template )
* @ return array with folders as key AND value
* @ throws Horde_Imap_Client_Exception
*/
public static function mailboxes ( Horde_Imap_Client_Socket $imap , array & $content = null )
{
// query all subscribed mailboxes
$mailboxes = $imap -> listMailboxes ( '*' , Horde_Imap_Client :: MBOX_SUBSCRIBED , array (
'special_use' => true ,
'attributes' => true , // otherwise special_use is only queried, but not returned ;-)
'delimiter' => true ,
));
//_debug_array($mailboxes);
// list mailboxes by special-use attributes
$folders = $attributes = $all = array ();
foreach ( $mailboxes as $mailbox => $data )
{
foreach ( $data [ 'attributes' ] as $attribute )
{
$attributes [ $attribute ][] = $mailbox ;
}
$folders [ $mailbox ] = $mailbox . ': ' . implode ( ', ' , $data [ 'attributes' ]);
}
// pre-select send, trash, ... folder for user, by checking special-use attributes or common name(s)
foreach ( array (
'acc_folder_sent' => array ( '\\sent' , 'sent' ),
'acc_folder_trash' => array ( '\\trash' , 'trash' ),
'acc_folder_draft' => array ( '\\drafts' , 'drafts' ),
'acc_folder_template' => array ( '' , 'templates' ),
'acc_folder_junk' => array ( '\\junk' , 'junk' , 'spam' ),
2017-05-31 15:10:58 +02:00
'acc_folder_ham' => array ( '' , 'ham' ),
2016-04-29 13:23:05 +02:00
'acc_folder_archive' => array ( '' , 'archive' ),
2016-03-28 20:51:38 +02:00
) as $name => $common_names )
{
2021-11-10 15:23:25 +01:00
unset ( $content [ $name ]);
2016-03-28 20:51:38 +02:00
// first check special-use attributes
if (( $special_use = array_shift ( $common_names )))
{
foreach (( array ) $attributes [ $special_use ] as $mailbox )
{
2021-11-10 15:23:25 +01:00
if ( empty ( $content [ $name ]) || is_string ( $mailbox ) && strlen ( $mailbox ) < strlen ( $content [ $name ]))
2016-03-28 20:51:38 +02:00
{
$content [ $name ] = $mailbox ;
}
}
}
// no special use folder found, try common names
if ( empty ( $content [ $name ]))
{
foreach ( $mailboxes as $mailbox => $data )
{
$delimiter = ! empty ( $data [ 'delimiter' ]) ? $data [ 'delimiter' ] : '.' ;
$name_parts = explode ( $delimiter , strtolower ( $mailbox ));
if ( array_intersect ( $name_parts , $common_names ) &&
2021-11-10 15:23:25 +01:00
( empty ( $content [ $name ]) || is_string ( $mailbox ) && strlen ( $mailbox ) < strlen ( $content [ $name ]) && substr ( $content [ $name ], 0 , 6 ) != 'INBOX' . $delimiter ))
2016-03-28 20:51:38 +02:00
{
//error_log(__METHOD__."() $mailbox --> ".substr($name, 11).' folder');
$content [ $name ] = $mailbox ;
}
//else error_log(__METHOD__."() $mailbox does NOT match array_intersect(".array2string($name_parts).', '.array2string($common_names).')='.array2string(array_intersect($name_parts, $common_names)));
}
}
$folders [( string ) $content [ $name ]] .= ' --> ' . substr ( $name , 11 ) . ' folder' ;
}
// uncomment for infos about selection process
//$content['folder_output'] = implode("\n", $folders);
return array_combine ( array_keys ( $mailboxes ), array_keys ( $mailboxes ));
}
/**
* Step 3 : Sieve
*
* @ param array $content
* @ param string $msg = ''
*/
public function sieve ( array $content , $msg = '' )
{
static $sieve_ssl2port = array (
self :: SSL_TLS => 5190 ,
self :: SSL_SSL => 5190 ,
self :: SSL_STARTTLS => array ( 4190 , 2000 ),
self :: SSL_NONE => array ( 4190 , 2000 ),
);
$content [ 'msg' ] = $msg ;
2021-10-06 16:07:47 +02:00
if ( ! empty ( $content [ 'button' ]))
2016-03-28 20:51:38 +02:00
{
2019-02-12 22:13:45 +01:00
$button = key ( $content [ 'button' ]);
2016-03-28 20:51:38 +02:00
unset ( $content [ 'button' ]);
switch ( $button )
{
case 'back' :
return $this -> folder ( $content );
case 'continue' :
if ( ! $content [ 'acc_sieve_enabled' ])
{
return $this -> smtp ( $content );
}
break ;
}
}
// first try: hide manual config
if ( ! isset ( $content [ 'acc_sieve_enabled' ]))
{
list (, $domain ) = explode ( '@' , $content [ 'acc_imap_username' ]);
$content [ 'acc_sieve_enabled' ] = ( int ) ! in_array ( $domain , self :: $no_sieve_blacklist );
$content [ 'manual_class' ] = 'emailadmin_manual' ;
}
else
{
unset ( $content [ 'manual_class' ]);
$readonlys [ 'button[manual]' ] = true ;
}
// set default ssl and port
2019-02-12 22:13:45 +01:00
if ( ! isset ( $content [ 'acc_sieve_ssl' ])) $content [ 'acc_sieve_ssl' ] = key ( self :: $ssl_types );
2016-03-28 20:51:38 +02:00
if ( empty ( $content [ 'acc_sieve_port' ])) $content [ 'acc_sieve_port' ] = $sieve_ssl2port [ $content [ 'acc_sieve_ssl' ]];
// check smtp connection
if ( $button == 'continue' )
{
$content [ 'sieve_connected' ] = false ;
$content [ 'sieve_output' ] = '' ;
unset ( $content [ 'manual_class' ]);
if ( empty ( $content [ 'acc_sieve_host' ]))
{
$content [ 'acc_sieve_host' ] = $content [ 'acc_imap_host' ];
}
// if use set non-standard port, use it
if ( ! in_array ( $content [ 'acc_sieve_port' ], ( array ) $sieve_ssl2port [ $content [ 'acc_sieve_ssl' ]]))
{
$data = array ( $content [ 'acc_sieve_ssl' ] => $content [ 'acc_sieve_port' ]);
}
else // otherwise try all standard ports
{
$data = $sieve_ssl2port ;
}
foreach ( $data as $ssl => $ports )
{
foreach (( array ) $ports as $port )
{
$content [ 'acc_sieve_ssl' ] = $ssl ;
$ssl_label = self :: $ssl_types [ $ssl ];
$e = null ;
try {
$content [ 'sieve_output' ] .= " \n " . Api\DateTime :: to ( 'now' , 'H:i:s' ) . " : Trying $ssl_label connection to $content[acc_sieve_host] : $port ... \n " ;
$content [ 'acc_sieve_port' ] = $port ;
$sieve = new Horde\ManageSieve ( array (
'host' => $content [ 'acc_sieve_host' ],
'port' => $content [ 'acc_sieve_port' ],
'secure' => self :: $ssl2secure [( string ) array_search ( $content [ 'acc_sieve_ssl' ], self :: $ssl2type )],
'timeout' => self :: TIMEOUT ,
'logger' => self :: DEBUG_LOG ? new admin_mail_logger ( self :: DEBUG_LOG ) : null ,
));
// connect to sieve server
$sieve -> connect ();
$content [ 'sieve_output' ] .= " \n " . lang ( 'Successful connected to %1 server%2.' , 'Sieve' , '' );
// and log in
$sieve -> login ( $content [ 'acc_imap_username' ], $content [ 'acc_imap_password' ]);
$content [ 'sieve_output' ] .= ' ' . lang ( 'and logged in' ) . " \n " ;
$content [ 'sieve_connected' ] = true ;
unset ( $content [ 'button' ]);
return $this -> smtp ( $content , lang ( 'Successful connected to %1 server%2.' , 'Sieve' ,
' ' . lang ( 'and logged in' )));
}
catch ( Horde\ManageSieve\Exception\ConnectionFailed $e ) {
$content [ 'sieve_output' ] .= " \n " . $e -> getMessage () . ' ' . $e -> details . " \n " ;
}
catch ( Exception $e ) {
$content [ 'sieve_output' ] .= " \n " . get_class ( $e ) . ': ' . $e -> getMessage () .
( $e -> details ? ' ' . $e -> details : '' ) . ' (' . $e -> getCode () . ')' . " \n " ;
$content [ 'sieve_output' ] .= $e -> getTraceAsString () . " \n " ;
if ( self :: $debug ) _egw_log_exception ( $e );
}
}
}
// not connected, and default ssl/port --> reset again to secure settings
if ( $data == $sieve_ssl2port )
{
2019-02-12 22:13:45 +01:00
$content [ 'acc_sieve_ssl' ] = key ( self :: $ssl_types );
2016-03-28 20:51:38 +02:00
$content [ 'acc_sieve_port' ] = $sieve_ssl2port [ $content [ 'acc_sieve_ssl' ]];
}
}
// add validation error, if we can identify a field
if ( ! $content [ 'sieve_connected' ] && $e instanceof Exception )
{
switch ( $e -> getCode ())
{
case 61 : // connection refused
case 60 : // connection timed out (imap.googlemail.com returns that for none-ssl/4190/2000)
case 65 : // no route ot host (imap.googlemail.com returns that for ssl/5190)
2016-04-27 21:12:20 +02:00
Etemplate :: set_validation_error ( 'acc_sieve_host' , lang ( $e -> getMessage ()));
Etemplate :: set_validation_error ( 'acc_sieve_port' , lang ( $e -> getMessage ()));
2016-03-28 20:51:38 +02:00
break ;
}
$content [ 'msg' ] = lang ( 'No sieve support detected, either fix configuration manually or leave it switched off.' );
$content [ 'acc_sieve_enabled' ] = 0 ;
}
$sel_options [ 'acc_sieve_ssl' ] = self :: $ssl_types ;
2016-04-27 21:12:20 +02:00
$tpl = new Etemplate ( 'admin.mailwizard.sieve' );
2016-03-28 20:51:38 +02:00
$tpl -> exec ( static :: APP_CLASS . 'sieve' , $content , $sel_options , $readonlys , $content , 2 );
}
/**
* Step 4 : SMTP
*
* @ param array $content
* @ param string $msg = ''
*/
public function smtp ( array $content , $msg = '' )
{
static $smtp_ssl2port = array (
self :: SSL_NONE => 25 ,
self :: SSL_SSL => 465 ,
self :: SSL_TLS => 465 ,
self :: SSL_STARTTLS => 587 ,
);
$content [ 'msg' ] = $msg ;
2021-10-06 16:07:47 +02:00
if ( ! empty ( $content [ 'button' ]))
2016-03-28 20:51:38 +02:00
{
2019-02-12 22:13:45 +01:00
$button = key ( $content [ 'button' ]);
2016-03-28 20:51:38 +02:00
unset ( $content [ 'button' ]);
switch ( $button )
{
case 'back' :
return $this -> sieve ( $content );
}
}
// first try: hide manual config
if ( ! isset ( $content [ 'acc_smtp_host' ]))
{
$content [ 'manual_class' ] = 'emailadmin_manual' ;
}
else
{
unset ( $content [ 'manual_class' ]);
$readonlys [ 'button[manual]' ] = true ;
}
// copy username/password from imap
if ( ! isset ( $content [ 'acc_smtp_username' ])) $content [ 'acc_smtp_username' ] = $content [ 'acc_imap_username' ];
if ( ! isset ( $content [ 'acc_smtp_password' ])) $content [ 'acc_smtp_password' ] = $content [ 'acc_imap_password' ];
// set default ssl
2019-02-12 22:13:45 +01:00
if ( ! isset ( $content [ 'acc_smtp_ssl' ])) $content [ 'acc_smtp_ssl' ] = key ( self :: $ssl_types );
2016-03-28 20:51:38 +02:00
if ( empty ( $content [ 'acc_smtp_port' ])) $content [ 'acc_smtp_port' ] = $smtp_ssl2port [ $content [ 'acc_smtp_ssl' ]];
// check smtp connection
if ( $button == 'continue' )
{
$content [ 'smtp_connected' ] = false ;
$content [ 'smtp_output' ] = '' ;
unset ( $content [ 'manual_class' ]);
if ( ! empty ( $content [ 'acc_smtp_host' ]))
{
$hosts = array ( $content [ 'acc_smtp_host' ] => true );
if (( string ) $content [ 'acc_smtp_ssl' ] !== ( string ) self :: SSL_TLS || $content [ 'acc_smtp_port' ] != $smtp_ssl2port [ $content [ 'acc_smtp_ssl' ]])
{
$ssl_type = ( string ) array_search ( $content [ 'acc_smtp_ssl' ], self :: $ssl2type );
$hosts [ $content [ 'acc_smtp_host' ]] = array (
$ssl_type => $content [ 'acc_smtp_port' ],
);
}
}
elseif ( $content [ 'ispdb' ] && ! empty ( $content [ 'ispdb' ][ 'smtp' ]))
{
$content [ 'smtp_output' ] .= lang ( 'Using data from Mozilla ISPDB for provider %1' , $content [ 'ispdb' ][ 'displayName' ]) . " \n " ;
$hosts = array ();
foreach ( $content [ 'ispdb' ][ 'smtp' ] as $server )
{
if ( ! isset ( $hosts [ $server [ 'hostname' ]]))
{
$hosts [ $server [ 'hostname' ]] = array ( 'username' => $server [ 'username' ]);
}
if ( strtoupper ( $server [ 'socketType' ]) == 'SSL' ) // try TLS first
{
$hosts [ $server [ 'hostname' ]][ 'TLS' ] = $server [ 'port' ];
}
$hosts [ $server [ 'hostname' ]][ strtoupper ( $server [ 'socketType' ])] = $server [ 'port' ];
// make sure we prefer SSL over STARTTLS over insecure
if ( count ( $hosts [ $server [ 'hostname' ]]) > 2 )
{
$hosts [ $server [ 'hostname' ]] = self :: fix_ssl_order ( $hosts [ $server [ 'hostname' ]]);
}
}
}
else
{
$hosts = $this -> guess_hosts ( $content [ 'ident_email' ], 'smtp' );
}
foreach ( $hosts as $host => $data )
{
$content [ 'acc_smtp_host' ] = $host ;
if ( ! is_array ( $data ))
{
$data = array ( 'TLS' => 465 , 'SSL' => 465 , 'STARTTLS' => 587 , '' => 25 );
}
foreach ( $data as $ssl => $port )
{
if ( $ssl === 'username' ) continue ;
$content [ 'acc_smtp_ssl' ] = ( int ) self :: $ssl2type [ $ssl ];
$e = null ;
try {
$content [ 'smtp_output' ] .= " \n " . Api\DateTime :: to ( 'now' , 'H:i:s' ) . " : Trying $ssl connection to $host : $port ... \n " ;
$content [ 'acc_smtp_port' ] = $port ;
2022-12-25 21:49:37 +01:00
$params = [
2016-03-28 20:51:38 +02:00
'username' => $content [ 'acc_smtp_username' ],
'password' => $content [ 'acc_smtp_password' ],
'host' => $content [ 'acc_smtp_host' ],
'port' => $content [ 'acc_smtp_port' ],
'secure' => self :: $ssl2secure [( string ) array_search ( $content [ 'acc_smtp_ssl' ], self :: $ssl2type )],
'timeout' => self :: TIMEOUT ,
'debug' => self :: DEBUG_LOG ,
2022-12-25 21:49:37 +01:00
];
if ( ! empty ( $content [ 'acc_oauth_provider_url' ]))
{
$params [ 'xoauth2_token' ] = self :: oauthToken ( $content , true );
}
$mail = new Horde_Mail_Transport_Smtphorde ( $params );
2016-03-28 20:51:38 +02:00
// create smtp connection and authenticate, if credentials given
$smtp = $mail -> getSMTPObject ();
$content [ 'smtp_output' ] .= " \n " . lang ( 'Successful connected to %1 server%2.' , 'SMTP' ,
( ! empty ( $content [ 'acc_smtp_username' ]) ? ' ' . lang ( 'and logged in' ) : '' )) . " \n " ;
if ( ! $smtp -> isSecureConnection ())
{
if ( ! empty ( $content [ 'acc_smtp_username' ]))
{
$content [ 'smtp_output' ] .= lang ( 'Connection is NOT secure! Everyone can read eg. your credentials.' ) . " \n " ;
}
$content [ 'acc_smtp_ssl' ] = 'no' ;
}
// Horde_Smtp always try to use STARTTLS, adjust our ssl-parameter if successful
elseif ( ! ( $content [ 'acc_smtp_ssl' ] > self :: SSL_NONE ))
{
//error_log(__METHOD__."() new Horde_Mail_Transport_Smtphorde(".array2string($params).")->getSMTPObject()->isSecureConnection()=".array2string($smtp->isSecureConnection()));
$content [ 'acc_smtp_ssl' ] = self :: SSL_STARTTLS ;
}
// try sending a mail to a different domain, if not authenticated, to see if that's required
if ( empty ( $content [ 'acc_smtp_username' ]))
{
$smtp -> send ( $content [ 'ident_email' ], 'noreply@example.com' , '' );
$content [ 'smtp_output' ] .= " \n " . lang ( 'Relay access checked' ) . " \n " ;
}
$content [ 'smtp_connected' ] = true ;
unset ( $content [ 'button' ]);
return $this -> edit ( $content , lang ( 'Successful connected to %1 server%2.' , 'SMTP' ,
empty ( $content [ 'acc_smtp_username' ]) ? ' - ' . lang ( 'Relay access checked' ) : ' ' . lang ( 'and logged in' )));
}
// unfortunately LOGIN_AUTHENTICATIONFAILED and SERVER_CONNECT are thrown as Horde_Mail_Exception
// while others are thrown as Horde_Smtp_Exception --> using common base Horde_Exception_Wrapped
catch ( Horde_Exception_Wrapped $e )
{
switch ( $e -> getCode ())
{
case Horde_Smtp_Exception :: LOGIN_AUTHENTICATIONFAILED :
case Horde_Smtp_Exception :: LOGIN_REQUIREAUTHENTICATION :
case Horde_Smtp_Exception :: UNSPECIFIED :
$content [ 'smtp_output' ] .= " \n " . $e -> getMessage () . " \n " ;
break ;
case Horde_Smtp_Exception :: SERVER_CONNECT :
$content [ 'smtp_output' ] .= " \n " . $e -> getMessage () . " \n " ;
break ;
default :
$content [ 'smtp_output' ] .= " \n " . $e -> getMessage () . ' (' . $e -> getCode () . ')' . " \n " ;
break ;
}
if ( self :: $debug ) _egw_log_exception ( $e );
}
catch ( Horde_Smtp_Exception $e )
{
// prever $e->details over $e->getMessage() as it contains original message from SMTP server (eg. relay access denied)
$content [ 'smtp_output' ] .= " \n " . ( empty ( $e -> details ) ? $e -> getMessage () . ' (' . $e -> getCode () . ')' : $e -> details ) . " \n " ;
//$content['smtp_output'] .= $e->getTraceAsString()."\n";
if ( self :: $debug ) _egw_log_exception ( $e );
}
catch ( Exception $e ) {
$content [ 'smtp_output' ] .= " \n " . get_class ( $e ) . ': ' . $e -> getMessage () . ' (' . $e -> getCode () . ')' . " \n " ;
//$content['smtp_output'] .= $e->getTraceAsString()."\n";
if ( self :: $debug ) _egw_log_exception ( $e );
}
}
}
}
// add validation error, if we can identify a field
if ( ! $content [ 'smtp_connected' ] && $e instanceof Horde_Exception_Wrapped )
{
switch ( $e -> getCode ())
{
case Horde_Smtp_Exception :: LOGIN_AUTHENTICATIONFAILED :
case Horde_Smtp_Exception :: LOGIN_REQUIREAUTHENTICATION :
case Horde_Smtp_Exception :: UNSPECIFIED :
2016-04-27 21:12:20 +02:00
Etemplate :: set_validation_error ( 'acc_smtp_username' , lang ( $e -> getMessage ()));
Etemplate :: set_validation_error ( 'acc_smtp_password' , lang ( $e -> getMessage ()));
2016-03-28 20:51:38 +02:00
break ;
case Horde_Smtp_Exception :: SERVER_CONNECT :
2016-04-27 21:12:20 +02:00
Etemplate :: set_validation_error ( 'acc_smtp_host' , lang ( $e -> getMessage ()));
Etemplate :: set_validation_error ( 'acc_smtp_port' , lang ( $e -> getMessage ()));
2016-03-28 20:51:38 +02:00
break ;
}
}
$sel_options [ 'acc_smtp_ssl' ] = self :: $ssl_types ;
2016-04-27 21:12:20 +02:00
$tpl = new Etemplate ( 'admin.mailwizard.smtp' );
2016-03-28 20:51:38 +02:00
$tpl -> exec ( static :: APP_CLASS . 'smtp' , $content , $sel_options , $readonlys , $content , 2 );
}
/**
* Edit mail account ( s )
*
* Gets either called with GET parameter :
*
* a ) account_id from admin >> Manage users to edit / add mail accounts for a user
* --> shows selectbox to switch between different mail accounts of user and " create new account "
*
* b ) via mail_wizard proxy class by regular mail user to edit ( acc_id GET parameter ) or create new mail account
*
* @ param array $content = null
* @ param string $msg = ''
* @ param string $msg_type = 'success'
*/
public function edit ( array $content = null , $msg = '' , $msg_type = 'success' )
{
// app is trying to tell something, while redirecting to wizard
if ( empty ( $content ) && $_GET [ 'acc_id' ] && empty ( $msg ) && ! empty ( $_GET [ 'msg' ]))
{
if ( stripos ( $_GET [ 'msg' ], 'fatal error:' ) !== false || $_GET [ 'msg_type' ] == 'error' ) $msg_type = 'error' ;
}
if ( $content [ 'acc_id' ] || ( isset ( $_GET [ 'acc_id' ]) && ( int ) $_GET [ 'acc_id' ] > 0 ) ) Mail :: unsetCachedObjects ( $content [ 'acc_id' ] ? $content [ 'acc_id' ] : $_GET [ 'acc_id' ]);
2016-04-27 21:12:20 +02:00
$tpl = new Etemplate ( 'admin.mailaccount' );
2016-03-28 20:51:38 +02:00
if ( ! is_array ( $content ) || ! empty ( $content [ 'acc_id' ]) && isset ( $content [ 'old_acc_id' ]) && $content [ 'acc_id' ] != $content [ 'old_acc_id' ])
{
if ( ! is_array ( $content )) $content = array ();
if ( $this -> is_admin && isset ( $_GET [ 'account_id' ]))
{
$content [ 'called_for' ] = ( int ) $_GET [ 'account_id' ];
$content [ 'accounts' ] = iterator_to_array ( Mail\Account :: search ( $content [ 'called_for' ]));
2021-10-06 16:07:47 +02:00
if ( ! empty ( $content [ 'accounts' ]))
2016-03-28 20:51:38 +02:00
{
2019-02-12 22:13:45 +01:00
$content [ 'acc_id' ] = key ( $content [ 'accounts' ]);
2016-03-28 20:51:38 +02:00
//error_log(__METHOD__.__LINE__.'.'.array2string($content['acc_id']));
2023-03-14 10:18:46 +01:00
// test if the "to be selected" account is imap or not
2019-03-03 14:39:57 +01:00
if ( is_array ( $content [ 'accounts' ]) && count ( $content [ 'accounts' ]) > 1 && Mail\Account :: is_multiple ( $content [ 'acc_id' ]))
2016-03-28 20:51:38 +02:00
{
try {
$account = Mail\Account :: read ( $content [ 'acc_id' ], $content [ 'called_for' ]);
//try to select the first account that is of type imap
if ( ! $account -> is_imap ())
{
2019-02-12 22:13:45 +01:00
$content [ 'acc_id' ] = key ( $content [ 'accounts' ]);
2016-03-28 20:51:38 +02:00
//error_log(__METHOD__.__LINE__.'.'.array2string($content['acc_id']));
}
}
catch ( Api\Exception\NotFound $e ) {
if ( self :: $debug ) _egw_log_exception ( $e );
}
}
}
if ( ! $content [ 'accounts' ]) // no email account, call wizard
{
return $this -> add ( array ( 'account_id' => ( int ) $_GET [ 'account_id' ]));
}
$content [ 'accounts' ][ 'new' ] = lang ( 'Create new account' );
}
if ( isset ( $_GET [ 'acc_id' ]) && ( int ) $_GET [ 'acc_id' ] > 0 )
{
$content [ 'acc_id' ] = ( int ) $_GET [ 'acc_id' ];
}
// clear current account-data, as account has changed and we going to read selected one
$content = array_intersect_key ( $content , array_flip ( array ( 'called_for' , 'accounts' , 'acc_id' , 'tabs' )));
2021-10-25 14:44:22 +02:00
if ( $content [ 'acc_id' ] === 'new' )
{
$content [ 'account_id' ] = $content [ 'called_for' ];
$content [ 'old_acc_id' ] = $content [ 'acc_id' ]; // to not call add/wizard, if we return from to
unset ( $content [ 'tabs' ]);
return $this -> add ( $content );
}
elseif ( $content [ 'acc_id' ] > 0 )
2016-03-28 20:51:38 +02:00
{
try {
2022-07-18 14:10:12 +02:00
$account = Mail\Account :: read ( $content [ 'acc_id' ], $this -> is_admin && ! empty ( $content [ 'called_for' ]) ?
$content [ 'called_for' ] : $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ]);
2016-03-28 20:51:38 +02:00
$account -> getUserData (); // quota, aliases, forwards etc.
$content += $account -> params ;
2023-03-14 10:18:46 +01:00
foreach ([ 'acc_imap_password' , 'acc_smtp_password' ] as $n )
{
if ( isset ( $content [ 'acc_oauth_username' ]) && $content [ $n ] === Mail\Credentials :: UNAVAILABLE )
{
unset ( $content [ $n ]);
}
}
2016-03-28 20:51:38 +02:00
$content [ 'acc_sieve_enabled' ] = ( string )( $content [ 'acc_sieve_enabled' ]);
$content [ 'notify_use_default' ] = ! $content [ 'notify_account_id' ];
self :: fix_account_id_0 ( $content [ 'account_id' ]);
// read identities (of current user) and mark std identity
$content [ 'identities' ] = iterator_to_array ( Mail\Account :: identities ( $account , true , 'name' , $content [ 'called_for' ]));
$content [ 'std_ident_id' ] = $content [ 'ident_id' ];
$content [ 'identities' ][ $content [ 'std_ident_id' ]] = lang ( 'Standard identity' );
// change self::SSL_NONE (=0) to "no" used in sel_options
foreach ( array ( 'imap' , 'smtp' , 'sieve' ) as $type )
{
if ( ! $content [ 'acc_' . $type . '_ssl' ]) $content [ 'acc_' . $type . '_ssl' ] = 'no' ;
}
}
catch ( Api\Exception\NotFound $e ) {
if ( self :: $debug ) _egw_log_exception ( $e );
2016-04-27 21:12:20 +02:00
Framework :: window_close ( lang ( 'Account not found!' ));
2016-03-28 20:51:38 +02:00
}
catch ( Exception $e ) {
if ( self :: $debug ) _egw_log_exception ( $e );
2016-04-27 21:12:20 +02:00
Framework :: window_close ( $e -> getMessage () . ' (' . get_class ( $e ) . ': ' . $e -> getCode () . ')' );
2016-03-28 20:51:38 +02:00
}
}
}
// some defaults for new accounts
if ( ! isset ( $content [ 'account_id' ]) || empty ( $content [ 'acc_id' ]) || $content [ 'acc_id' ] === 'new' )
{
if ( ! isset ( $content [ 'account_id' ])) $content [ 'account_id' ] = array ( $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ]);
$content [ 'acc_user_editable' ] = $content [ 'acc_further_identities' ] = true ;
$readonlys [ 'ident_id' ] = true ; // need to create standard identity first
}
if ( empty ( $content [ 'acc_name' ]))
{
$content [ 'acc_name' ] = $content [ 'ident_email' ];
}
// disable some stuff for non-emailadmins (all values are preserved!)
if ( ! $this -> is_admin )
{
$readonlys = array (
'account_id' => true , 'button[multiple]' => true , 'acc_user_editable' => true ,
'acc_further_identities' => true ,
'acc_imap_type' => true , 'acc_imap_logintype' => true , 'acc_domain' => true ,
2022-07-05 15:16:01 +02:00
'acc_imap_admin_username' => true , 'acc_imap_admin_password' => true , 'acc_imap_admin_use_without_pw' => true ,
2016-03-28 20:51:38 +02:00
'acc_smtp_type' => true , 'acc_smtp_auth_session' => true ,
);
}
// ensure correct values for single user mail accounts (we only hide them client-side)
if ( ! ( $is_multiple = Mail\Account :: is_multiple ( $content )))
{
$content [ 'acc_imap_type' ] = 'EGroupware\\Api\\Mail\\Imap' ;
unset ( $content [ 'acc_imap_login_type' ]);
$content [ 'acc_smtp_type' ] = 'EGroupware\\Api\\Mail\\Smtp' ;
unset ( $content [ 'acc_smtp_auth_session' ]);
unset ( $content [ 'notify_use_default' ]);
}
2016-10-28 14:27:07 +02:00
// copy ident_email_alias selectbox back to regular name
2017-03-30 16:05:09 +02:00
elseif ( isset ( $content [ 'ident_email_alias' ]) && ! empty ( $content [ 'ident_email_alias' ]))
2016-10-28 14:27:07 +02:00
{
$content [ 'ident_email' ] = $content [ 'ident_email_alias' ];
}
2016-04-27 21:12:20 +02:00
$edit_access = Mail\Account :: check_access ( Acl :: EDIT , $content );
2016-03-28 20:51:38 +02:00
// disable notification save-default and use-default, if only one account or no edit-rights
$tpl -> disableElement ( 'notify_save_default' , ! $is_multiple || ! $edit_access );
$tpl -> disableElement ( 'notify_use_default' , ! $is_multiple );
2021-10-06 16:07:47 +02:00
if ( ! empty ( $content [ 'button' ]))
2016-03-28 20:51:38 +02:00
{
2019-02-12 22:13:45 +01:00
$button = key ( $content [ 'button' ]);
2016-03-28 20:51:38 +02:00
unset ( $content [ 'button' ]);
switch ( $button )
{
case 'wizard' :
// if we just came from wizard, go back to last page/step
if ( isset ( $content [ 'smtp_connected' ]))
{
return $this -> smtp ( $content );
}
// otherwise start with first step
return $this -> autoconfig ( $content );
case 'delete_identity' :
// delete none-standard identity of current user
if (( $this -> is_admin || $content [ 'acc_further_identities' ]) &&
$content [ 'ident_id' ] > 0 && $content [ 'std_ident_id' ] != $content [ 'ident_id' ])
{
Mail\Account :: delete_identity ( $content [ 'ident_id' ]);
$msg = lang ( 'Identity deleted' );
unset ( $content [ 'identities' ][ $content [ 'ident_id' ]]);
$content [ 'ident_id' ] = $content [ 'std_ident_id' ];
}
break ;
case 'save' :
case 'apply' :
try {
// save none-standard identity for current user
if ( $content [ 'acc_id' ] && $content [ 'acc_id' ] !== 'new' &&
( $this -> is_admin || $content [ 'acc_further_identities' ]) &&
$content [ 'std_ident_id' ] != $content [ 'ident_id' ])
{
$content [ 'ident_id' ] = Mail\Account :: save_identity ( array (
'account_id' => $content [ 'called_for' ] ? $content [ 'called_for' ] : $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ],
) + $content );
$content [ 'identities' ][ $content [ 'ident_id' ]] = Mail\Account :: identity_name ( $content );
$msg = lang ( 'Identity saved.' );
if ( $edit_access ) $msg .= ' ' . lang ( 'Switch back to standard identity to save account.' );
}
elseif ( $edit_access )
{
// if admin username/password given, check if it is valid
$account = new Mail\Account ( $content );
if ( $account -> acc_imap_administration )
{
2024-07-06 10:38:49 +02:00
try {
$imap = $account -> imapServer ( true );
if ( $imap ) $imap -> checkAdminConnection ();
}
catch ( \Horde_Imap_Client_Exception $e ) {
Api\Json\Response :: get () -> message ( lang ( 'Checking admin credentials failed' ) . ': ' . $e -> getMessage (), 'info' );
}
2016-03-28 20:51:38 +02:00
}
// test sieve connection, if not called for other user, enabled and credentials available
if ( ! $content [ 'called_for' ] && $account -> acc_sieve_enabled && $account -> acc_imap_username )
{
$account -> imapServer () -> retrieveRules ();
}
2021-10-25 14:44:22 +02:00
$new_account = ! (( int ) $content [ 'acc_id' ] > 0 );
2016-03-28 20:51:38 +02:00
// check for deliveryMode="forwardOnly", if a forwarding-address is given
if ( $content [ 'acc_smtp_type' ] != 'EGroupware\\Api\\Mail\\Smtp' &&
$content [ 'deliveryMode' ] == Mail\Smtp :: FORWARD_ONLY &&
empty ( $content [ 'mailForwardingAddress' ]))
{
2016-04-27 21:12:20 +02:00
Etemplate :: set_validation_error ( 'mailForwardingAddress' , lang ( 'Field must not be empty !!!' ));
2016-03-28 20:51:38 +02:00
throw new Api\Exception\WrongUserinput ( lang ( 'You need to specify a forwarding address, when checking "%1"!' , lang ( 'Forward only' )));
}
// set notifications to store according to checkboxes
if ( $content [ 'notify_save_default' ])
{
$content [ 'notify_account_id' ] = 0 ;
}
elseif ( ! $content [ 'notify_use_default' ])
{
$content [ 'notify_account_id' ] = $content [ 'called_for' ] ?
$content [ 'called_for' ] : $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ];
}
2017-08-28 12:35:21 +02:00
// SMIME SAVE
2017-08-30 17:00:28 +02:00
if ( isset ( $content [ 'smimeKeyUpload' ]))
2017-08-28 12:35:21 +02:00
{
2018-04-10 14:52:14 +02:00
$content [ 'acc_smime_cred_id' ] = self :: save_smime_key ( $content , $tpl , $content [ 'called_for' ]);
unset ( $content [ 'smimeKeyUpload' ]);
2017-08-28 12:35:21 +02:00
}
2016-03-28 20:51:38 +02:00
self :: fix_account_id_0 ( $content [ 'account_id' ], true );
2021-10-06 12:31:23 +02:00
$content = Mail\Account :: write ( $content , ! empty ( $content [ 'called_for' ]) && $this -> is_admin ?
2016-03-28 20:51:38 +02:00
$content [ 'called_for' ] : $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ]);
self :: fix_account_id_0 ( $content [ 'account_id' ]);
$msg = lang ( 'Account saved.' );
// user wants default notifications
if ( $content [ 'acc_id' ] && $content [ 'notify_use_default' ])
{
// delete own ones
Mail\Notifications :: delete ( $content [ 'acc_id' ], $content [ 'called_for' ] ?
$content [ 'called_for' ] : $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ]);
// load default ones
$content = array_merge ( $content , Mail\Notifications :: read ( $content [ 'acc_id' ], 0 ));
}
// add new std identity entry
if ( $new_account )
{
$content [ 'std_ident_id' ] = $content [ 'ident_id' ];
$content [ 'identities' ] = array (
$content [ 'std_ident_id' ] => lang ( 'Standard identity' ));
}
if ( isset ( $content [ 'accounts' ]))
{
if ( ! isset ( $content [ 'accounts' ][ $content [ 'acc_id' ]])) // insert new account as top, not bottom
{
$content [ 'accounts' ] = array ( $content [ 'acc_id' ] => '' ) + $content [ 'accounts' ];
}
$content [ 'accounts' ][ $content [ 'acc_id' ]] = Mail\Account :: identity_name ( $content , false );
}
}
else
{
if ( $content [ 'notify_use_default' ] && $content [ 'notify_account_id' ])
{
// delete own ones
if ( Mail\Notifications :: delete ( $content [ 'acc_id' ], $content [ 'called_for' ] ?
$content [ 'called_for' ] : $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ]))
{
$msg = lang ( 'Notification folders updated.' );
}
// load default ones
$content = array_merge ( $content , Mail\Notifications :: read ( $content [ 'acc_id' ], 0 ));
}
2017-04-24 14:28:08 +02:00
if ( ! $content [ 'notify_use_default' ] && is_array ( $content [ 'notify_folders' ]))
2016-03-28 20:51:38 +02:00
{
$content [ 'notify_account_id' ] = $content [ 'called_for' ] ?
$content [ 'called_for' ] : $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ];
if ( Mail\Notifications :: write ( $content [ 'acc_id' ], $content [ 'notify_account_id' ],
$content [ 'notify_folders' ]))
{
$msg = lang ( 'Notification folders updated.' );
}
}
if ( $content [ 'acc_user_forward' ] && ! empty ( $content [ 'acc_smtp_type' ]) && $content [ 'acc_smtp_type' ] != 'EGroupware\\Api\\Mail\\Smtp' )
{
$account = new Mail\Account ( $content );
$account -> smtpServer () -> saveSMTPForwarding ( $content [ 'called_for' ] ?
$content [ 'called_for' ] : $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ],
$content [ 'mailForwardingAddress' ],
$content [ 'forwardOnly' ] ? null : 'yes' );
}
2018-04-10 13:15:09 +02:00
// smime (private) key uploaded by user himself
if ( ! empty ( $content [ 'smimeKeyUpload' ]))
{
2018-04-10 14:52:14 +02:00
$content [ 'acc_smime_cred_id' ] = self :: save_smime_key ( $content , $tpl );
unset ( $content [ 'smimeKeyUpload' ]);
2018-04-10 13:15:09 +02:00
}
2016-03-28 20:51:38 +02:00
}
}
catch ( Horde_Imap_Client_Exception $e )
{
_egw_log_exception ( $e );
2016-06-24 14:28:49 +02:00
$tpl -> set_validation_error ( 'acc_imap_admin_username' , $msg = lang ( $e -> getMessage ()) . ( $e -> details ? ', ' . lang ( $e -> details ) : '' ));
2016-03-28 20:51:38 +02:00
$msg_type = 'error' ;
$content [ 'tabs' ] = 'admin.mailaccount.imap' ; // should happen automatic
break ;
}
catch ( Horde\ManageSieve\Exception\ConnectionFailed $e )
{
_egw_log_exception ( $e );
$tpl -> set_validation_error ( 'acc_sieve_port' , $msg = lang ( $e -> getMessage ()));
$msg_type = 'error' ;
$content [ 'tabs' ] = 'admin.mailaccount.sieve' ; // should happen automatic
break ;
}
catch ( Exception $e ) {
$msg = lang ( 'Error saving account!' ) . " \n " . $e -> getMessage ();
$button = 'apply' ;
$msg_type = 'error' ;
}
if ( $content [ 'acc_id' ]) Mail :: unsetCachedObjects ( $content [ 'acc_id' ]);
if ( stripos ( $msg , 'fatal error:' ) !== false ) $msg_type = 'error' ;
2021-09-01 16:23:38 +02:00
Framework :: refresh_opener ( $msg , 'mail-account' , $content [ 'acc_id' ], $new_account ? 'add' : 'update' , null , null , null , $msg_type );
2016-04-27 21:12:20 +02:00
if ( $button == 'save' ) Framework :: window_close ();
2016-03-28 20:51:38 +02:00
break ;
case 'delete' :
2016-04-27 21:12:20 +02:00
if ( ! Mail\Account :: check_access ( Acl :: DELETE , $content ))
2016-03-28 20:51:38 +02:00
{
$msg = lang ( 'Permission denied!' );
$msg_type = 'error' ;
}
elseif ( Mail\Account :: delete ( $content [ 'acc_id' ]) > 0 )
{
if ( $content [ 'acc_id' ]) Mail :: unsetCachedObjects ( $content [ 'acc_id' ]);
2021-09-01 16:23:38 +02:00
Framework :: refresh_opener ( lang ( 'Account deleted.' ), 'mail-account' , $content [ 'acc_id' ], 'delete' );
2016-04-27 21:12:20 +02:00
Framework :: window_close ();
2016-03-28 20:51:38 +02:00
}
else
{
$msg = lang ( 'Failed to delete account!' );
$msg_type = 'error' ;
}
}
}
2017-09-13 16:05:43 +02:00
// SMIME UPLOAD/DELETE/EXPORT control
$content [ 'hide_smime_upload' ] = false ;
2018-04-10 14:52:14 +02:00
if ( ! empty ( $content [ 'acc_smime_cred_id' ]))
2017-09-13 16:05:43 +02:00
{
if ( ! empty ( $content [ 'smime_delete_p12' ]) &&
Mail\Credentials :: delete (
$content [ 'acc_id' ],
$content [ 'called_for' ] ? $content [ 'called_for' ] : $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ],
Mail\Credentials :: SMIME
))
{
2018-04-10 14:52:14 +02:00
unset ( $content [ 'acc_smime_password' ], $content [ 'smimeKeyUpload' ], $content [ 'smime_delete_p12' ], $content [ 'acc_smime_cred_id' ]);
2017-09-13 16:05:43 +02:00
$content [ 'hide_smime_upload' ] = false ;
}
else
{
2018-04-10 13:15:09 +02:00
// do NOT send smime private key to client side, it's unnecessary and binary blob breaks json encoding
2018-04-10 14:52:14 +02:00
$content [ 'acc_smime_password' ] = Mail\Credentials :: UNAVAILABLE ;
2018-04-10 13:15:09 +02:00
2017-09-13 16:05:43 +02:00
$content [ 'hide_smime_upload' ] = true ;
}
}
2016-03-28 20:51:38 +02:00
// disable delete button for new, not yet saved entries, if no delete rights or a non-standard identity selected
$readonlys [ 'button[delete]' ] = empty ( $content [ 'acc_id' ]) ||
2016-04-27 21:12:20 +02:00
! Mail\Account :: check_access ( Acl :: DELETE , $content ) ||
2016-03-28 20:51:38 +02:00
$content [ 'ident_id' ] != $content [ 'std_ident_id' ];
// if account is for multiple user, change delete confirmation to reflect that
if ( Mail\Account :: is_multiple ( $content ))
{
$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') " );
}
// if no edit access, make whole dialog readonly
if ( ! $edit_access )
{
$readonlys [ '__ALL__' ] = true ;
$readonlys [ 'button[cancel]' ] = false ;
// allow to edit notification-folders
$readonlys [ 'button[save]' ] = $readonlys [ 'button[apply]' ] =
2018-04-10 13:15:09 +02:00
$readonlys [ 'notify_folders' ] = $readonlys [ 'notify_use_default' ] = false ;
// allow to edit sMime stuff
$readonlys [ 'smimeGenerate' ] = $readonlys [ 'smimeKeyUpload' ] = $readonlys [ 'smime_pkcs12_password' ] =
$readonlys [ 'smime_export_p12' ] = $readonlys [ 'smime_delete_p12' ] = false ;
2016-03-28 20:51:38 +02:00
}
$sel_options [ 'acc_imap_ssl' ] = $sel_options [ 'acc_sieve_ssl' ] =
$sel_options [ 'acc_smtp_ssl' ] = self :: $ssl_types ;
// admin access to account with no credentials available
2023-01-25 13:38:40 +01:00
if ( $this -> is_admin && ( ! empty ( $content [ 'called_for' ]) || empty ( $content [ 'acc_imap_host' ]) || $content [ 'called_for' ]) ||
// if OAuth failed, do not try to connect and trigger next authentication(-failure), but show failure message
! empty ( $content [ 'oauth_failure' ]))
2016-03-28 20:51:38 +02:00
{
2022-07-20 19:11:59 +02:00
// can't connection to imap --> allow free entries in taglists
2016-03-28 20:51:38 +02:00
foreach ( array ( 'acc_folder_sent' , 'acc_folder_trash' , 'acc_folder_draft' , 'acc_folder_template' , 'acc_folder_junk' ) as $folder )
{
$tpl -> setElementAttribute ( $folder , 'allowFreeEntries' , true );
}
}
else
{
try {
2023-11-21 14:56:41 +01:00
if (( $oauth = OpenIDConnectClient :: providerByDomain (
2023-01-16 23:56:30 +01:00
$content [ 'acc_oauth_username' ] ? ? $content [ 'acc_imap_username' ] ? ? $content [ 'ident_email' ], $content [ 'acc_imap_host' ])))
{
$content += self :: oauth2content ( $oauth );
}
2016-03-28 20:51:38 +02:00
$sel_options [ 'acc_folder_sent' ] = $sel_options [ 'acc_folder_trash' ] =
$sel_options [ 'acc_folder_draft' ] = $sel_options [ 'acc_folder_template' ] =
2017-05-31 15:10:58 +02:00
$sel_options [ 'acc_folder_junk' ] = $sel_options [ 'acc_folder_archive' ] =
$sel_options [ 'notify_folders' ] = $sel_options [ 'acc_folder_ham' ] =
2016-03-28 20:51:38 +02:00
self :: mailboxes ( self :: imap_client ( $content ));
2017-10-06 17:38:26 +02:00
// Allow folder notification on INBOX for popup_only
if ( $GLOBALS [ 'egw_info' ][ 'user' ][ 'preferences' ][ 'notifications' ][ 'notification_chain' ] == 'popup_only' )
{
$sel_options [ 'notify_folders' ][ 'INBOX' ] = lang ( 'INBOX' );
}
2016-03-28 20:51:38 +02:00
}
catch ( Exception $e ) {
if ( self :: $debug ) _egw_log_exception ( $e );
// let user know what the problem is and that he can fix it using wizard or deleting
$msg = lang ( $e -> getMessage ()) . " \n \n " . lang ( 'You can use wizard to fix account settings or delete account.' );
$msg_type = 'error' ;
// cant connection to imap --> allow free entries in taglists
foreach ( array ( 'acc_folder_sent' , 'acc_folder_trash' , 'acc_folder_draft' , 'acc_folder_template' , 'acc_folder_junk' ) as $folder )
{
$tpl -> setElementAttribute ( $folder , 'allowFreeEntries' , true );
}
}
}
$sel_options [ 'acc_imap_type' ] = Mail\Types :: getIMAPServerTypes ( false );
$sel_options [ 'acc_smtp_type' ] = Mail\Types :: getSMTPServerTypes ( false );
$sel_options [ 'acc_imap_logintype' ] = self :: $login_types ;
$sel_options [ 'ident_id' ] = $content [ 'identities' ];
$sel_options [ 'acc_id' ] = $content [ 'accounts' ];
2016-10-28 14:27:07 +02:00
$sel_options [ 'acc_further_identities' ] = self :: $further_identities ;
2016-03-28 20:51:38 +02:00
// user is allowed to create or edit further identities
if ( $edit_access || $content [ 'acc_further_identities' ])
{
$sel_options [ 'ident_id' ][ 'new' ] = lang ( 'Create new identity' );
$readonlys [ 'ident_id' ] = false ;
// if no edit-access and identity is not standard identity --> allow to edit identity
if ( ! $edit_access && $content [ 'ident_id' ] != $content [ 'std_ident_id' ])
{
$readonlys += array (
'button[save]' => false , 'button[apply]' => false ,
'button[placeholders]' => false ,
'ident_name' => false ,
2016-10-28 14:46:17 +02:00
'ident_realname' => false , 'ident_email' => false , 'ident_email_alias' => false ,
2016-03-28 20:51:38 +02:00
'ident_org' => false , 'ident_signature' => false ,
);
}
if ( $content [ 'ident_id' ] != $content [ 'old_ident_id' ] &&
( $content [ 'old_ident_id' ] || $content [ 'ident_id' ] != $content [ 'std_ident_id' ]))
{
2021-10-11 15:10:27 +02:00
if (( int ) $content [ 'ident_id' ] > 0 )
2016-03-28 20:51:38 +02:00
{
$identity = Mail\Account :: read_identity ( $content [ 'ident_id' ], false , $content [ 'called_for' ]);
unset ( $identity [ 'account_id' ]);
2016-10-28 14:53:51 +02:00
$content = array_merge ( $content , $identity , array ( 'ident_email_alias' => $identity [ 'ident_email' ]));
2016-03-28 20:51:38 +02:00
}
else
{
$content [ 'ident_name' ] = $content [ 'ident_realname' ] = $content [ 'ident_email' ] =
2016-10-28 14:53:51 +02:00
$content [ 'ident_email_alias' ] = $content [ 'ident_org' ] = $content [ 'ident_signature' ] = '' ;
2016-03-28 20:51:38 +02:00
}
if ( empty ( $msg ) && $edit_access && $content [ 'ident_id' ] && $content [ 'ident_id' ] != $content [ 'std_ident_id' ])
{
$msg = lang ( 'Switch back to standard identity to save other account data.' );
$msg_type = 'help' ;
}
$content [ 'old_ident_id' ] = $content [ 'ident_id' ];
}
}
$content [ 'old_acc_id' ] = $content [ 'acc_id' ];
2017-07-12 19:12:14 +02:00
// if only aliases are allowed for futher identities, add them as options
// allow admins to always add arbitrary aliases
if ( $content [ 'acc_further_identities' ] == 2 && ! $this -> is_admin )
{
$sel_options [ 'ident_email_alias' ] = array_merge (
array ( '' => $content [ 'mailLocalAddress' ] . ' (' . lang ( 'Default' ) . ')' ),
2021-10-12 11:44:24 +02:00
array_combine ( $content [ 'mailAlternateAddress' ] ? ? [], $content [ 'mailAlternateAddress' ] ? ? []));
2017-07-12 19:12:14 +02:00
// if admin explicitly set a non-alias, we need to add it to aliases to keep it after storing signature by user
if ( $content [ 'ident_email' ] !== $content [ 'mailLocalAddress' ] && ! isset ( $sel_options [ 'ident_email_alias' ][ $content [ 'ident_email' ]]))
{
$sel_options [ 'ident_email_alias' ][ $content [ 'ident_email' ]] = $content [ 'ident_email' ];
}
// copy ident_email to select-box ident_email_alias, as et2 requires unique ids
$content [ 'ident_email_alias' ] = $content [ 'ident_email' ];
$content [ 'select_ident_mail' ] = true ;
}
2016-03-28 20:51:38 +02:00
// only allow to delete further identities, not a standard identity
$readonlys [ 'button[delete_identity]' ] = ! ( $content [ 'ident_id' ] > 0 && $content [ 'ident_id' ] != $content [ 'std_ident_id' ]);
// disable aliases tab for default smtp class EGroupware\Api\Mail\Smtp
$readonlys [ 'tabs' ][ 'admin.mailaccount.aliases' ] = ! $content [ 'acc_smtp_type' ] ||
$content [ 'acc_smtp_type' ] == 'EGroupware\\Api\\Mail\\Smtp' ;
2016-10-28 14:27:07 +02:00
if ( $readonlys [ 'tabs' ][ 'admin.mailaccount.aliases' ])
{
unset ( $sel_options [ 'acc_further_identities' ][ 2 ]); // can limit identities to aliases without aliases ;-)
}
2016-03-28 20:51:38 +02:00
// allow smtp class to disable certain features in alias tab
if ( $content [ 'acc_smtp_type' ] && class_exists ( $content [ 'acc_smtp_type' ]) &&
is_a ( $content [ 'acc_smtp_type' ], 'EGroupware\\Api\\Mail\\Smtp\\Ldap' , true ))
{
$content [ 'no_forward_available' ] = ! constant ( $content [ 'acc_smtp_type' ] . '::FORWARD_ATTR' );
if ( ! constant ( $content [ 'acc_smtp_type' ] . '::FORWARD_ONLY_ATTR' ))
{
$readonlys [ 'deliveryMode' ] = true ;
}
}
// account allows users to change forwards
if ( ! $edit_access && ! $readonlys [ 'tabs' ][ 'admin.mailaccount.aliases' ] && $content [ 'acc_user_forward' ])
{
$readonlys [ 'mailForwardingAddress' ] = false ;
}
// allow imap classes to disable certain tabs or fields
if (( $class = Mail\Account :: getIcClass ( $content [ 'acc_imap_type' ])) && class_exists ( $class ) &&
( $imap_ro = call_user_func ( array ( $class , 'getUIreadonlys' ))))
{
$readonlys = array_merge ( $readonlys , $imap_ro , array (
'tabs' => array_merge (( array ) $readonlys [ 'tabs' ], ( array ) $imap_ro [ 'tabs' ]),
));
}
2016-04-27 21:12:20 +02:00
Framework :: message ( $msg ? $msg : ( string ) $_GET [ 'msg' ], $msg_type );
2016-03-28 20:51:38 +02:00
// when called by admin for existing accounts, display further administrative actions
2021-10-25 14:44:22 +02:00
if ( $content [ 'called_for' ] && ( int ) $content [ 'acc_id' ] > 0 )
2016-03-28 20:51:38 +02:00
{
$admin_actions = array ();
2016-04-27 21:12:20 +02:00
foreach ( Api\Hooks :: process ( array (
2016-03-28 20:51:38 +02:00
'location' => 'emailadmin_edit' ,
'account_id' => $content [ 'called_for' ],
'acc_id' => $content [ 'acc_id' ],
)) as $actions )
{
if ( $actions ) $admin_actions = array_merge ( $admin_actions , $actions );
}
if ( $admin_actions ) $tpl -> setElementAttribute ( 'admin_actions' , 'actions' , $admin_actions );
}
$content [ 'admin_actions' ] = ( bool ) $admin_actions ;
2017-04-24 11:39:02 +02:00
//try to fix identities with no domain part set e.g. alias as identity
if ( ! strpos ( $content [ 'ident_email' ], '@' ))
{
$content [ 'ident_email' ] = Mail :: fixInvalidAliasAddress ( Api\Accounts :: id2name ( $content [ 'acc_imap_account_id' ], 'account_email' ), $content [ 'ident_email' ]);
}
2020-06-08 21:30:57 +02:00
// If no EPL available, show that in spamtitan blur
2021-02-08 16:31:22 +01:00
$content [ 'spamtitan_blur' ] = $GLOBALS [ 'egw_info' ][ 'user' ][ 'apps' ][ 'stylite' ] ? '' : lang ( 'SpamTitan integration requires EPL version' );
2020-06-08 21:30:57 +02:00
2016-03-28 20:51:38 +02:00
$tpl -> exec ( static :: APP_CLASS . 'edit' , $content , $sel_options , $readonlys , $content , 2 );
}
2018-04-10 13:15:09 +02:00
/**
* Saves the smime key
*
* @ param array $content
* @ param Etemplate $tpl
* @ param int $account_id = null account to save smime key for , default current user
2018-04-10 14:52:14 +02:00
* @ return int cred_id or null on error
2018-04-10 13:15:09 +02:00
*/
private static function save_smime_key ( array $content , Etemplate $tpl , $account_id = null )
{
if (( $pkcs12 = file_get_contents ( $content [ 'smimeKeyUpload' ][ 'tmp_name' ])))
{
$cert_info = Mail\Smime :: extractCertPKCS12 ( $pkcs12 , $content [ 'smime_pkcs12_password' ]);
if ( is_array ( $cert_info ) && ! empty ( $cert_info [ 'cert' ]))
{
// save public key
$smime = new Mail\Smime ;
$email = $smime -> getEmailFromKey ( $cert_info [ 'cert' ]);
$AB_bo = new addressbook_bo ();
$AB_bo -> set_smime_keys ( array (
$email => $cert_info [ 'cert' ]
));
// save private key
if ( ! isset ( $account_id )) $account_id = $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ];
2018-04-10 14:52:14 +02:00
return Mail\Credentials :: write ( $content [ 'acc_id' ], $email , $pkcs12 , Mail\Credentials :: SMIME , $account_id );
2018-04-10 13:15:09 +02:00
}
2018-04-10 14:52:14 +02:00
$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!' ));
2018-04-10 13:15:09 +02:00
}
2018-04-10 14:52:14 +02:00
return null ;
2018-04-10 13:15:09 +02:00
}
2016-03-28 20:51:38 +02:00
/**
* Replace 0 with '' or back
*
* @ param string | array & $account_id on return always array
* @ param boolean $back = false
*/
private static function fix_account_id_0 ( & $account_id = null , $back = false )
{
if ( ! isset ( $account_id )) return ;
if ( ! is_array ( $account_id ))
{
2022-07-20 19:11:59 +02:00
$account_id = $account_id ? explode ( ',' , $account_id ) : [];
}
if ( $back && ! $account_id )
{
$account_id = 0 ;
2016-03-28 20:51:38 +02:00
}
2022-07-20 19:11:59 +02:00
if ( ! $back && count ( $account_id ) === 1 && ! current ( $account_id ))
2016-03-28 20:51:38 +02:00
{
2022-07-20 19:11:59 +02:00
$account_id = [];
2016-03-28 20:51:38 +02:00
}
}
/**
* Instanciate imap - client
*
* @ param array $content
* @ param int $timeout = null default use value returned by Mail\Imap :: getTimeOut ()
* @ return Horde_Imap_Client_Socket
*/
2022-12-23 21:32:54 +01:00
protected static function imap_client ( array & $content , $timeout = null )
2016-03-28 20:51:38 +02:00
{
2022-12-23 21:32:54 +01:00
$config = [
2016-03-28 20:51:38 +02:00
'username' => $content [ 'acc_imap_username' ],
'password' => $content [ 'acc_imap_password' ],
'hostspec' => $content [ 'acc_imap_host' ],
'port' => $content [ 'acc_imap_port' ],
'secure' => self :: $ssl2secure [( string ) array_search ( $content [ 'acc_imap_ssl' ], self :: $ssl2type )],
'timeout' => $timeout > 0 ? $timeout : Mail\Imap :: getTimeOut (),
'debug' => self :: DEBUG_LOG ,
2022-12-23 21:32:54 +01:00
];
2023-01-16 23:56:30 +01:00
if ( ! empty ( $content [ 'acc_oauth_provider_url' ]) || ! empty ( $content [ 'acc_oauth_access_token' ]))
2022-12-23 21:32:54 +01:00
{
$config [ 'xoauth2_token' ] = self :: oauthToken ( $content );
2023-01-16 23:56:30 +01:00
$config [ 'username' ] = $content [ 'acc_oauth_username' ] ? ? $content [ 'acc_imap_username' ];
2022-12-25 21:49:37 +01:00
if ( empty ( $config [ 'password' ])) $config [ 'password' ] = '**oauth**' ; // some password is required, even if not used
2022-12-23 21:32:54 +01:00
}
return new Horde_Imap_Client_Socket ( $config );
}
/**
* Acquire OAuth access ( and refresh ) token
*/
2022-12-25 21:49:37 +01:00
protected static function oauthToken ( array & $content , bool $smtp = false )
2022-12-23 21:32:54 +01:00
{
2023-01-16 23:56:30 +01:00
if ( empty ( $content [ 'acc_oauth_access_token' ]))
2022-12-23 21:32:54 +01:00
{
2023-01-16 23:56:30 +01:00
if ( empty ( $content [ 'acc_oauth_client_secret' ]) &&
( $oauth = OpenIDConnectClient :: providerByDomain ( $content [ 'acc_oauth_username' ] ? ? $content [ 'acc_imap_username' ] ? ? $content [ 'ident_email' ], $content [ 'acc_imap_host' ])))
{
$content += self :: oauth2content ( $oauth );
}
if ( empty ( $content [ 'acc_oauth_client_secret' ]))
{
throw new Exception ( lang ( " No OAuth client secret for provider '%1'! " , $content [ 'acc_oauth_provider_url' ]));
}
$oidc = new OpenIDConnectClient ( $content [ 'acc_oauth_provider_url' ],
$content [ 'acc_oauth_client_id' ], $content [ 'acc_oauth_client_secret' ]);
2022-12-23 21:32:54 +01:00
2023-01-16 23:56:30 +01:00
// Office365 requires client-ID as appid GET parameter (https://github.com/jumbojett/OpenID-Connect-PHP/issues/190)
if ( ! empty ( $content [ OpenIDConnectClient :: ADD_CLIENT_TO_WELL_KNOWN ]))
{
$oidc -> setWellKnownConfigParameters ([ $content [ OpenIDConnectClient :: ADD_CLIENT_TO_WELL_KNOWN ] => $content [ 'acc_oauth_client_id' ]]);
}
// Google requires access_type=offline&prompt=consent to return a refresh-token
if ( ! empty ( $content [ OpenIDConnectClient :: ADD_AUTH_PARAM ]))
{
2023-01-25 13:38:40 +01:00
$oidc -> addAuthParam ( str_replace ( '$username' , $content [ 'acc_oauth_username' ] ? ? $content [ 'acc_imap_username' ] ? ? $content [ 'ident_email' ], $content [ OpenIDConnectClient :: ADD_AUTH_PARAM ]));
2023-01-16 23:56:30 +01:00
}
2022-12-23 21:32:54 +01:00
2023-01-16 23:56:30 +01:00
// we need to use response_code=query / GET request to keep our session token!
$oidc -> setResponseTypes ([ 'code' ]); // to be able to use query, not 'id_token'
//$oidc->setAllowImplicitFlow(true);
$oidc -> addScope ( $content [ 'acc_oauth_scopes' ]);
}
2022-12-23 21:32:54 +01:00
2023-11-21 14:56:41 +01:00
if ( ! empty ( $content [ 'acc_oauth_access_token' ]) ||
! empty ( $content [ 'acc_oauth_refresh_token' ]) && $content [ 'acc_oauth_refresh_token' ] !== Mail\Credentials :: UNAVAILABLE )
2022-12-23 21:32:54 +01:00
{
if ( empty ( $content [ 'acc_oauth_access_token' ]))
{
2023-11-21 14:56:41 +01:00
$content [ 'acc_oauth_access_token' ] = $oidc -> refreshToken ( $content [ 'acc_oauth_refresh_token' ]) -> access_token ;
2022-12-23 21:32:54 +01:00
}
2023-11-21 14:56:41 +01:00
if ( ! empty ( $content [ 'acc_oauth_access_token' ]))
2022-12-25 21:49:37 +01:00
{
2023-11-21 14:56:41 +01:00
if ( $smtp )
{
return new Horde_Smtp_Password_Xoauth2 ( $content [ 'acc_oauth_username' ] ? ? $content [ 'acc_smtp_username' ], $content [ 'acc_oauth_access_token' ]);
}
return new Horde_Imap_Client_Password_Xoauth2 ( $content [ 'acc_oauth_username' ] ? ? $content [ 'acc_imap_username' ], $content [ 'acc_oauth_access_token' ]);
2022-12-25 21:49:37 +01:00
}
2022-12-23 21:32:54 +01:00
}
2022-12-25 21:49:37 +01:00
// Run OAuth authentication, will NOT return, but call success or failure callbacks below
2022-12-23 21:32:54 +01:00
$oidc -> authenticateThen ( __CLASS__ . '::oauthAuthenticated' , [ $content ], __CLASS__ . '::oauthFailure' , [ $content ]);
}
/**
* Oauth success callback calling autoconfig again
*
* @ param OpenIDConnectClient $oidc
* @ param array $content
* @ return void
*/
public static function oauthAuthenticated ( OpenIDConnectClient $oidc , array $content )
{
2022-12-25 21:49:37 +01:00
if ( empty ( $content [ 'acc_oauth_username' ]))
{
2023-01-16 23:56:30 +01:00
$content [ 'acc_oauth_username' ] = $content [ 'acc_imap_username' ] ? ? $oidc -> getVerifiedClaims ( 'email' ) ? ? $content [ 'ident_email' ];
2022-12-25 21:49:37 +01:00
}
if ( empty ( $content [ 'acc_oauth_refresh_token' ] = $oidc -> getRefreshToken ()))
{
$content [ 'output' ] .= lang ( 'OAuth Authentiction' ) . ': ' . lang ( 'Successfull, but NO refresh-token received!' );
$content [ 'connected' ] = false ;
}
2022-12-23 21:32:54 +01:00
$content [ 'acc_oauth_access_token' ] = $oidc -> getAccessToken ();
2023-02-20 15:36:52 +01:00
if ( empty ( $GLOBALS [ 'egw_info' ][ 'user' ][ 'apps' ][ 'admin' ]))
{
$GLOBALS [ 'egw_info' ][ 'flags' ][ 'currentapp' ] = 'mail' ;
$obj = new mail_wizard ();
}
else
{
$GLOBALS [ 'egw_info' ][ 'flags' ][ 'currentapp' ] = 'admin' ;
$obj = new self ;
}
2023-01-25 13:38:40 +01:00
unset ( $content [ 'oauth_failure' ]);
2023-01-16 23:56:30 +01:00
if ( ! empty ( $content [ 'acc_id' ]))
{
2023-01-25 13:38:40 +01:00
$content [ 'button' ] = [ 'save' => true ]; // automatic save token, refresh mail app and close popup
2023-01-16 23:56:30 +01:00
$obj -> edit ( $content , lang ( 'Use save or apply to store the received OAuth token!' ), 'info' );
}
else
{
$obj -> autoconfig ( $content );
}
2022-12-23 21:32:54 +01:00
}
/**
* Oauth failure callback calling autoconfig again
*
* @ param OpenIDConnectClientException | null $exception
* @ param array $content
*/
public static function oauthFailure ( Throwable $exception = null , array $content )
{
2023-02-20 15:36:52 +01:00
if ( empty ( $GLOBALS [ 'egw_info' ][ 'user' ][ 'apps' ][ 'admin' ]))
{
$GLOBALS [ 'egw_info' ][ 'flags' ][ 'currentapp' ] = 'mail' ;
$obj = new mail_wizard ();
}
else
{
$GLOBALS [ 'egw_info' ][ 'flags' ][ 'currentapp' ] = 'admin' ;
$obj = new self ;
}
2023-01-25 13:38:40 +01:00
$content [ 'oauth_failure' ] = $exception ? : true ;
2023-01-16 23:56:30 +01:00
if ( ! empty ( $content [ 'acc_id' ]))
{
$obj -> edit ( $content , lang ( 'OAuth Authentiction' ) . ': ' . ( $exception ? $exception -> getMessage () : lang ( 'failed' )), 'error' );
}
else
{
$content [ 'output' ] .= lang ( 'OAuth Authentiction' ) . ': ' . ( $exception ? $exception -> getMessage () : lang ( 'failed' ));
$content [ 'connected' ] = false ;
$obj -> autoconfig ( $content );
}
2022-12-23 21:32:54 +01:00
$obj -> autoconfig ( $content );
2016-03-28 20:51:38 +02:00
}
/**
* Reorder SSL types to make sure we start with TLS , SSL , STARTTLS and insecure last
*
* @ param array $data ssl => port pairs plus other data like value for 'username'
* @ return array
*/
protected static function fix_ssl_order ( $data )
{
$ordered = array ();
foreach ( array_merge ( array ( 'TLS' , 'SSL' , 'STARTTLS' ), array_keys ( $data )) as $key )
{
if ( array_key_exists ( $key , $data )) $ordered [ $key ] = $data [ $key ];
}
return $ordered ;
}
/**
* Query Mozilla ' s ISPDB
*
* Some providers eg . 1 - and - 1 do not report their hosted domains to ISPDB ,
* therefore we try it with the found MX and it ' s domain - part ( host - name removed ) .
*
* @ param string $domain domain or email
* @ param boolean $try_mx = true if domain itself is not found , try mx or domain - part ( host removed ) of mx
* @ return array with values for keys 'displayName' , 'imap' , 'smtp' , 'pop3' , which each contain
* array of arrays with values for keys 'hostname' , 'port' , 'socketType' = ( SSL | STARTTLS ), 'username' =% EMAILADDRESS %
*/
protected static function mozilla_ispdb ( $domain , $try_mx = true )
{
if ( strpos ( $domain , '@' ) !== false ) list (, $domain ) = explode ( '@' , $domain );
$url = 'https://autoconfig.thunderbird.net/v1.1/' . $domain ;
try {
2021-10-25 14:44:22 +02:00
$xml = simplexml_load_string ( file_get_contents ( $url ) ? : '' );
if ( ! $xml || ! $xml -> emailProvider ) throw new Api\Exception\NotFound ();
2016-03-28 20:51:38 +02:00
$provider = array (
'displayName' => ( string ) $xml -> emailProvider -> displayName ,
);
foreach ( $xml -> emailProvider -> children () as $tag => $server )
{
if ( ! in_array ( $tag , array ( 'incomingServer' , 'outgoingServer' ))) continue ;
foreach ( $server -> attributes () as $name => $value )
{
if ( $name == 'type' ) $type = ( string ) $value ;
}
$data = array ();
foreach ( $server as $name => $value )
{
foreach ( $value -> children () as $tag => $val )
{
$data [ $name ][ $tag ] = ( string ) $val ;
}
if ( ! isset ( $data [ $name ])) $data [ $name ] = ( string ) $value ;
}
$provider [ $type ][] = $data ;
}
}
catch ( Exception $e ) {
// ignore own not-found exception or xml parsing execptions
unset ( $e );
if ( $try_mx && ( $dns = dns_get_record ( $domain , DNS_MX )))
{
$domain = $dns [ 0 ][ 'target' ];
if ( ! ( $provider = self :: mozilla_ispdb ( $domain , false )))
{
list (, $domain ) = explode ( '.' , $domain , 2 );
$provider = self :: mozilla_ispdb ( $domain , false );
}
}
else
{
$provider = array ();
}
}
//error_log(__METHOD__."('$email') returning ".array2string($provider));
return $provider ;
}
/**
* Guess possible server hostnames from email address :
* - $type . $domain , mail . $domain
* - replace host in MX with imap or mail
* - MX for $domain
*
* @ param string $email email address
* @ param string $type = 'imap' 'imap' or 'smtp' , used as hostname beside 'mail'
* @ return array of hostname => true pairs
*/
protected function guess_hosts ( $email , $type = 'imap' )
{
list (, $domain ) = explode ( '@' , $email );
$hosts = array ();
// try usuall names
$hosts [ $type . '.' . $domain ] = true ;
$hosts [ 'mail.' . $domain ] = true ;
if ( $type == 'smtp' ) $hosts [ 'send.' . $domain ] = true ;
if (( $dns = dns_get_record ( $domain , DNS_MX )))
{
//error_log(__METHOD__."('$email') dns_get_record('$domain', DNS_MX) returned ".array2string($dns));
// hosts for office365 are outlook|smpt.office365.com for MX *.mail.protection.outlook.com
if ( substr ( $dns [ 0 ][ 'target' ], - 28 ) == '.mail.protection.outlook.com' )
{
$hosts [( $type == 'imap' ? 'outlook' : 'smtp' ) . '.office365.com' ] = true ;
}
$hosts [ preg_replace ( '/^[^.]+/' , $type , $dns [ 0 ][ 'target' ])] = true ;
$hosts [ preg_replace ( '/^[^.]+/' , 'mail' , $dns [ 0 ][ 'target' ])] = true ;
if ( $type == 'smtp' ) $hosts [ preg_replace ( '/^[^.]+/' , 'send' , $dns [ 0 ][ 'target' ])] = true ;
$hosts [ $dns [ 0 ][ 'target' ]] = true ;
}
// verify hosts in dns
foreach ( array_keys ( $hosts ) as $host )
{
if ( ! dns_get_record ( $host , DNS_A )) unset ( $hosts [ $host ]);
}
//error_log(__METHOD__."('$email') returning ".array2string($hosts));
return $hosts ;
}
/**
* Set mail account status wheter to 'active' or '' ( inactive )
*
* @ param array $_data account an array of data called via long task running dialog
* $_data : array (
* id => account_id ,
2024-04-10 11:58:53 +02:00
* quota => quotaLimit ,
2016-03-28 20:51:38 +02:00
* domain => mailLocalAddress ,
* status => mail activation status ( 'active' | '' )
* )
2020-01-29 11:08:44 +01:00
* @ param string $etemplate_exec_id to check against CSRF
2016-03-28 20:51:38 +02:00
* @ return json response
*/
2020-01-29 11:08:44 +01:00
public function ajax_activeAccounts ( $_data , $etemplate_exec_id )
2016-03-28 20:51:38 +02:00
{
2020-01-29 11:08:44 +01:00
Api\Etemplate\Request :: csrfCheck ( $etemplate_exec_id , __METHOD__ , func_get_args ());
2016-03-28 20:51:38 +02:00
if ( ! $this -> is_admin ) die ( 'no rights to be here!' );
2016-04-27 21:12:20 +02:00
$response = Api\Json\Response :: get ();
2016-03-28 20:51:38 +02:00
if (( $account = $GLOBALS [ 'egw' ] -> accounts -> read ( $_data [ 'id' ])))
{
2024-04-10 11:58:53 +02:00
if ( $_data [ 'quota' ] !== '' || $_data [ 'accountStatus' ] !== '' || strpos ( $_data [ 'domain' ], '.' ))
2016-03-28 20:51:38 +02:00
{
2024-04-10 12:29:13 +02:00
$ea_account = Mail\Account :: get_default ( false , false , false , true , $_data [ 'id' ], true );
if ( ! $ea_account || ! Mail\Account :: is_multiple ( $ea_account ))
2016-03-28 20:51:38 +02:00
{
2024-04-10 12:29:13 +02:00
$msg = $account [ 'account_fullname' ] . ' (#' . $_data [ 'id' ] . '): ' . lang ( 'No default account found!' );
2016-03-28 20:51:38 +02:00
return $response -> data ( $msg );
}
2024-04-10 12:29:13 +02:00
if ( $ea_account && ( $userData = $ea_account -> getUserData ()))
2016-03-28 20:51:38 +02:00
{
$userData = array (
'acc_smtp_type' => $ea_account -> acc_smtp_type ,
'accountStatus' => $_data [ 'status' ],
2024-04-10 11:58:53 +02:00
'quotaLimit' => $_data [ 'quota' ] ? : $userData [ 'quotaLimit' ],
'mailLocalAddress' => $userData [ 'mailLocalAddress' ],
2016-03-28 20:51:38 +02:00
);
if ( strpos ( $_data [ 'domain' ], '.' ) !== false )
{
2021-10-25 14:44:22 +02:00
$userData [ 'mailLocalAddress' ] = preg_replace ( '/@' . preg_quote ( $ea_account -> acc_domain , '/' ) . '$/' , '@' . $_data [ 'domain' ], $userData [ 'mailLocalAddress' ]);
2016-03-28 20:51:38 +02:00
foreach ( $userData [ 'mailAlternateAddress' ] as & $alias )
{
2021-10-25 14:44:22 +02:00
$alias = preg_replace ( '/@' . preg_quote ( $ea_account -> acc_domain , '/' ) . '$/' , '@' . $_data [ 'domain' ], $alias );
2016-03-28 20:51:38 +02:00
}
}
2024-04-10 11:58:53 +02:00
// fulfill the saveUserData requirements
2016-03-28 20:51:38 +02:00
$userData += $ea_account -> params ;
$ea_account -> saveUserData ( $_data [ 'id' ], $userData );
2024-04-10 12:29:13 +02:00
$msg = $account [ 'account_fullname' ] . ' (#' . $_data [ 'id' ] . '): ' .
2024-04-10 11:58:53 +02:00
( $userData [ 'accountStatus' ] === 'active' ? lang ( 'activated' ) : lang ( 'deactivated' ));
2016-03-28 20:51:38 +02:00
}
else
{
2024-04-10 12:29:13 +02:00
$msg = lang ( 'No profile defined for user %1' , $account [ 'account_fullname' ] . ' (#' . $_data [ 'id' ] . " ) \n " );
2016-03-28 20:51:38 +02:00
}
}
}
$response -> data ( $msg );
}
}
/**
* Trivial file logger , as Horde\ManageSieve does not support just a file
*/
class admin_mail_logger
{
private $fp ;
public function __construct ( $log )
{
$this -> fp = is_resource ( $log ) ? $log : fopen ( $log , 'a' );
}
public function debug ( $msg )
{
fwrite ( $this -> fp , $msg . " \n " );
}
2023-01-25 13:38:40 +01:00
}